Skip to main content

bamboo_server/app_state/
builder.rs

1use super::init::{
2    build_provider_handles, build_schedule_manager, build_spawn_scheduler, init_mcp_manager,
3    init_metrics_service, init_schedule_store, init_skill_manager, init_storage,
4    load_permission_checker, spawn_runner_cleanup_task,
5};
6use super::tools::{build_base_tools, build_root_tools};
7use super::*;
8use crate::tools::OptionalSubagentModelResolver;
9use bamboo_agent_core::storage::Storage;
10
11impl AppState {
12    /// Create unified app state with direct provider access
13    ///
14    /// This eliminates the proxy pattern where we created an AgentAppState
15    /// that called back to web_service via HTTP. Now we have direct provider access.
16    ///
17    /// # Arguments
18    ///
19    /// * `bamboo_home_dir` - Bamboo home directory containing all application data.
20    ///   This is the root directory (e.g., `${HOME}/.bamboo`) that contains:
21    ///   - config.json: Configuration file
22    ///   - sessions/: Conversation history
23    ///   - skills/: Skill definitions
24    ///   - workflows/: Workflow definitions
25    ///   - cache/: Cached data
26    ///   - runtime/: Runtime files
27    ///
28    /// # Returns
29    ///
30    /// A fully initialized AppState with all components ready for use.
31    /// # Example
32    ///
33    /// ```rust,no_run
34    /// use bamboo_server::app_state::AppState;
35    /// use std::path::PathBuf;
36    ///
37    /// #[tokio::main]
38    /// async fn main() {
39    ///     let state = AppState::new(PathBuf::from("/path/to/bamboo-data-dir"))
40    ///         .await
41    ///         .expect("failed to initialize app state");
42    ///     let provider = state.get_provider().await;
43    ///     let _models = provider.list_models().await.ok();
44    /// }
45    /// ```
46    pub async fn new(bamboo_home_dir: PathBuf) -> Result<Self, AppError> {
47        // Ensure all helpers that rely on `core::paths::bamboo_dir()` see the same
48        // directory as the server runtime.
49        bamboo_infrastructure::paths::init_bamboo_dir(bamboo_home_dir.clone());
50
51        // Load config from the specified data directory
52        let config = Config::from_data_dir(Some(bamboo_home_dir.clone()));
53
54        let provider_registry = match bamboo_infrastructure::ProviderRegistry::from_config(
55            &config,
56            bamboo_home_dir.clone(),
57        )
58        .await
59        {
60            Ok(registry) => Arc::new(registry),
61            Err(e) => {
62                tracing::error!("Failed to create provider registry: {}", e);
63                Arc::new(
64                    bamboo_infrastructure::ProviderRegistry::from_config(
65                        &Config::default(),
66                        bamboo_home_dir.clone(),
67                    )
68                    .await
69                    .expect("Cannot create even an empty provider registry"),
70                )
71            }
72        };
73
74        let provider = provider_registry.get_default().unwrap_or_else(|| {
75            let default_provider_name = provider_registry.default_provider_name();
76            let message = if config.has_provider_instances() {
77                format!(
78                    "Default provider instance '{}' is not available or failed to initialize",
79                    default_provider_name
80                )
81            } else {
82                format!(
83                    "Provider '{}' is not available or failed to initialize",
84                    config.provider
85                )
86            };
87            Arc::new(UnconfiguredProvider { message }) as Arc<dyn LLMProvider>
88        });
89
90        Self::new_with_provider(bamboo_home_dir, config, provider).await
91    }
92
93    /// Create unified app state with a specific provider
94    ///
95    /// Allows injecting a custom LLM provider instead of creating
96    /// one from configuration. Useful for testing and custom deployments.
97    ///
98    /// # Arguments
99    ///
100    /// * `bamboo_home_dir` - Bamboo home directory containing all application data
101    /// * `config` - Application configuration
102    /// * `provider` - Pre-configured LLM provider implementation
103    ///
104    /// # Returns
105    ///
106    /// A fully initialized AppState with the provided provider.
107    pub async fn new_with_provider(
108        bamboo_home_dir: PathBuf,
109        config: Config,
110        provider: Arc<dyn LLMProvider>,
111    ) -> Result<Self, AppError> {
112        // Wire the configured-default-workspace resolver into agent-core. This keeps
113        // the dependency arrow pointing down (agent-core no longer imports the
114        // infrastructure config crate); the server owns config and provides it here.
115        bamboo_agent_core::workspace_state::set_default_workspace_provider(|| {
116            bamboo_infrastructure::Config::from_data_dir(Some(
117                bamboo_infrastructure::paths::bamboo_dir(),
118            ))
119            .get_default_work_area_path()
120        });
121
122        let data_dir = bamboo_home_dir.clone();
123        let (session_store, storage) = init_storage(&data_dir).await?;
124        let persistence = Arc::new(LockedSessionStore::new(storage.clone()));
125
126        // In-memory session cache (shared across handlers and background jobs).
127        let sessions: Arc<RwLock<HashMap<String, bamboo_agent_core::Session>>> =
128            Arc::new(RwLock::new(HashMap::new()));
129
130        let config = Arc::new(RwLock::new(config));
131
132        let permission_checker = load_permission_checker(&bamboo_home_dir).await;
133        let mcp_manager = init_mcp_manager(config.clone());
134        let skill_manager = init_skill_manager(&data_dir).await;
135        let metrics_service = init_metrics_service(&data_dir).await?;
136
137        let startup_sessions = {
138            let entries = session_store.list_index_entries().await;
139            let mut sessions = Vec::new();
140            for entry in entries {
141                if let Some(session) = session_store
142                    .load_session(&entry.id)
143                    .await
144                    .map_err(AppError::StorageError)?
145                {
146                    sessions.push(session);
147                }
148            }
149            sessions
150        };
151        metrics_service
152            .reconcile_startup_sessions(startup_sessions, &[])
153            .await
154            .map_err(|error| {
155                AppError::InternalError(anyhow::anyhow!(
156                    "Failed to reconcile stale metrics state on startup: {error}"
157                ))
158            })?;
159
160        let agent_runners: Arc<RwLock<HashMap<String, AgentRunner>>> =
161            Arc::new(RwLock::new(HashMap::new()));
162        spawn_runner_cleanup_task(agent_runners.clone(), None);
163
164        let process_registry = Arc::new(ProcessRegistry::new());
165        let (provider_lock, provider_handle) = build_provider_handles(provider);
166
167        // Initialize multi-provider registry (for features.provider_model_ref).
168        let config_snapshot = config.read().await;
169        let provider_registry = match bamboo_infrastructure::ProviderRegistry::from_config(
170            &config_snapshot,
171            bamboo_home_dir.clone(),
172        )
173        .await
174        {
175            Ok(registry) => Arc::new(registry),
176            Err(e) => {
177                tracing::error!("Failed to create provider registry: {}", e);
178                Arc::new(
179                    bamboo_infrastructure::ProviderRegistry::from_config(
180                        &Config::default(),
181                        bamboo_home_dir.clone(),
182                    )
183                    .await
184                    .expect("Cannot create even an empty provider registry"),
185                )
186            }
187        };
188        drop(config_snapshot);
189
190        let provider_router = Arc::new(bamboo_infrastructure::ProviderModelRouter::new(
191            provider_registry.clone(),
192        ));
193        let model_catalog = Arc::new(bamboo_infrastructure::ModelCatalogService::new(
194            provider_registry.clone(),
195        ));
196
197        let base_tools = build_base_tools(
198            config.clone(),
199            permission_checker,
200            mcp_manager.clone(),
201            skill_manager.clone(),
202            storage.clone(),
203            persistence.clone(),
204            sessions.clone(),
205            bamboo_home_dir.clone(),
206        );
207
208        // Long-lived session event senders map (UI subscriptions + background tasks).
209        let session_event_senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>> =
210            Arc::new(RwLock::new(HashMap::new()));
211
212        // Account-scoped durable change feed. Opening the journal recovers the
213        // max seq so the sequence counter stays monotonic across restarts.
214        let account_sink = crate::events::AccountEventSink::new(data_dir.join("events"))
215            .map_err(|e| {
216                AppError::InternalError(anyhow::anyhow!(
217                    "failed to initialize account change-feed journal: {e}"
218                ))
219            })?;
220
221        // Subagent profile registry: built-ins + user/project/env overrides.
222        // Loaded here (rather than further down) so we can wrap the child
223        // tool executor with `PolicyAwareToolExecutor` before it is handed
224        // to the spawn scheduler. Workspace path is intentionally `None` —
225        // the registry is a process-wide singleton; per-workspace overrides
226        // can still be picked up via `BAMBOO_SUBAGENT_PROFILES_FILE` or by
227        // resolving against `<bamboo_home_dir>/subagent_profiles.json`.
228        let subagent_profiles = crate::subagent_profiles::load_registry(&bamboo_home_dir, None)
229            .map_err(|e| {
230                crate::error::AppError::InternalError(anyhow::anyhow!(
231                    "failed to load subagent profile registry: {e}"
232                ))
233            })?;
234
235        // Child tools intentionally do not expose `SubAgent` (no nested
236        // child spawns). They are wrapped by `PolicyAwareToolExecutor` so
237        // that each child's `subagent_type` metadata is consulted to
238        // enforce its `ToolPolicy` (allow/deny/inherit) at tool-call time.
239        let child_tools: Arc<dyn bamboo_agent_core::tools::ToolExecutor> =
240            Arc::new(crate::tools::PolicyAwareToolExecutor::new(
241                base_tools.clone(),
242                subagent_profiles.clone(),
243                sessions.clone(),
244            ));
245
246        // Unified agent runtime (shared resources for all execution paths).
247        // default_tools = base_tools (builtin + MCP + memory + skills) as a safe fallback.
248        // Interactive execution paths pass an explicit tool surface override:
249        // root sessions use ToolSurface::Root; child sessions use ToolSurface::Child.
250        let agent = Arc::new(
251            bamboo_engine::Agent::builder()
252                .storage(storage.clone())
253                .persistence(persistence.clone())
254                .attachment_reader(session_store.clone())
255                .skill_manager(skill_manager.clone())
256                .metrics_collector(metrics_service.collector())
257                .config(config.clone())
258                .provider(provider_handle.clone())
259                .default_tools(base_tools.clone())
260                .build()
261                .expect("agent runtime should be fully configured"),
262        );
263
264        let child_completion_coordinator = Arc::new(
265            super::child_completion_coordinator::ChildCompletionCoordinator::new(
266                storage.clone(),
267                persistence.clone(),
268                sessions.clone(),
269                agent_runners.clone(),
270                session_event_senders.clone(),
271                agent.clone(),
272                config.clone(),
273                provider_registry.clone(),
274                provider_router.clone(),
275                data_dir.clone(),
276                Some(account_sink.inbox()),
277            ),
278        );
279
280        // Initialize sub-session spawn scheduler (async background jobs).
281        let config_snapshot = config.read().await.clone();
282        let external_runner =
283            crate::external_agents::runtime::build_external_child_runner(&config_snapshot);
284        let spawn_scheduler = build_spawn_scheduler(
285            agent.clone(),
286            child_tools,
287            sessions.clone(),
288            agent_runners.clone(),
289            session_event_senders.clone(),
290            external_runner,
291            Some(provider_router.clone()),
292            Some(child_completion_coordinator.clone()),
293            Some(data_dir.clone()),
294            Some(account_sink.inbox()),
295        );
296
297        let tools_with_task = base_tools.clone();
298
299        let schedule_store = init_schedule_store(&data_dir).await?;
300        let schedule_manager = build_schedule_manager(
301            schedule_store.clone(),
302            agent.clone(),
303            tools_with_task.clone(),
304            sessions.clone(),
305            agent_runners.clone(),
306            session_event_senders.clone(),
307            persistence.clone(),
308            config.clone(),
309            provider_registry.clone(),
310            Some(data_dir.clone()),
311            Some(account_sink.inbox()),
312        );
313
314        bamboo_engine::auto_dream::spawn_auto_dream_task(
315            bamboo_engine::auto_dream::AutoDreamContext {
316                session_store: session_store.clone(),
317                storage: storage.clone(),
318                provider: provider_handle.clone(),
319                config: config.clone(),
320                provider_registry: provider_registry.clone(),
321            },
322        );
323
324        let config_for_resolver = config.clone();
325        let subagent_model_resolver: OptionalSubagentModelResolver = {
326            let registry = provider_registry.clone();
327            Some(Arc::new(
328                move |subagent_type: String| -> futures::future::BoxFuture<
329                    'static,
330                    Option<bamboo_domain::ProviderModelRef>,
331                > {
332                    let config_for_resolver = config_for_resolver.clone();
333                    let registry = registry.clone();
334                    Box::pin(async move {
335                        let config_snap = config_for_resolver.read().await.clone();
336                        bamboo_engine::model_config_helper::resolve_subagent_model_ref(
337                            &config_snap,
338                            &config_snap.provider,
339                            &registry,
340                            &subagent_type,
341                        )
342                    })
343                },
344            ))
345        };
346
347        let tools = build_root_tools(
348            tools_with_task.clone(),
349            schedule_store.clone(),
350            schedule_manager.clone(),
351            session_store.clone(),
352            storage.clone(),
353            persistence.clone(),
354            spawn_scheduler.clone(),
355            sessions.clone(),
356            agent_runners.clone(),
357            session_event_senders.clone(),
358            subagent_model_resolver,
359            config.clone(),
360            subagent_profiles.clone(),
361        );
362
363        child_completion_coordinator
364            .set_root_tools(tools.clone())
365            .await;
366
367        let tool_factory =
368            crate::tools::ToolSurfaceFactory::new(base_tools, tools_with_task, tools);
369
370        Ok(Self {
371            app_data_dir: bamboo_home_dir,
372            config,
373            provider: provider_lock,
374            provider_handle,
375            sessions,
376            storage,
377            session_store,
378            persistence,
379            spawn_scheduler,
380            child_completion_coordinator,
381            schedule_store,
382            schedule_manager,
383            tool_factory,
384            subagent_profiles,
385            cancel_tokens: Arc::new(RwLock::new(HashMap::new())),
386            skill_manager,
387            mcp_manager,
388            metrics_service,
389            agent_runners,
390            session_event_senders,
391            account_sink,
392            process_registry,
393            metrics_bus: None, // Will be set by server if needed
394            agent,
395            provider_registry,
396            provider_router,
397            model_catalog,
398            title_gen_in_flight: Arc::new(dashmap::DashSet::new()),
399        })
400    }
401}