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_config::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 =
55            match bamboo_llm::ProviderRegistry::from_config(&config, bamboo_home_dir.clone()).await
56            {
57                Ok(registry) => Arc::new(registry),
58                Err(e) => {
59                    tracing::error!("Failed to create provider registry: {}", e);
60                    Arc::new(
61                        bamboo_llm::ProviderRegistry::from_config(
62                            &Config::default(),
63                            bamboo_home_dir.clone(),
64                        )
65                        .await
66                        .expect("Cannot create even an empty provider registry"),
67                    )
68                }
69            };
70
71        let provider = provider_registry.get_default().unwrap_or_else(|| {
72            let default_provider_name = provider_registry.default_provider_name();
73            let message = if config.has_provider_instances() {
74                format!(
75                    "Default provider instance '{}' is not available or failed to initialize",
76                    default_provider_name
77                )
78            } else {
79                format!(
80                    "Provider '{}' is not available or failed to initialize",
81                    config.provider
82                )
83            };
84            Arc::new(UnconfiguredProvider { message }) as Arc<dyn LLMProvider>
85        });
86
87        Self::new_with_provider(bamboo_home_dir, config, provider).await
88    }
89
90    /// Create unified app state with a specific provider
91    ///
92    /// Allows injecting a custom LLM provider instead of creating
93    /// one from configuration. Useful for testing and custom deployments.
94    ///
95    /// # Arguments
96    ///
97    /// * `bamboo_home_dir` - Bamboo home directory containing all application data
98    /// * `config` - Application configuration
99    /// * `provider` - Pre-configured LLM provider implementation
100    ///
101    /// # Returns
102    ///
103    /// A fully initialized AppState with the provided provider.
104    pub async fn new_with_provider(
105        bamboo_home_dir: PathBuf,
106        config: Config,
107        provider: Arc<dyn LLMProvider>,
108    ) -> Result<Self, AppError> {
109        // Wire the configured-default-workspace resolver into agent-core. This keeps
110        // the dependency arrow pointing down (agent-core no longer imports the
111        // infrastructure config crate); the server owns config and provides it here.
112        bamboo_agent_core::workspace_state::set_default_workspace_provider(|| {
113            bamboo_llm::Config::from_data_dir(Some(bamboo_config::paths::bamboo_dir()))
114                .get_default_work_area_path()
115        });
116
117        let data_dir = bamboo_home_dir.clone();
118        let (session_store, storage) = init_storage(&data_dir).await?;
119        let persistence = Arc::new(LockedSessionStore::new(storage.clone()));
120
121        // In-memory session cache (shared across handlers and background jobs).
122        let sessions: bamboo_engine::SessionCache = Arc::new(dashmap::DashMap::new());
123
124        let config = Arc::new(RwLock::new(config));
125
126        let permission_checker = load_permission_checker(&bamboo_home_dir).await;
127        let notification_service = Arc::new(bamboo_notification::NotificationService::new(
128            bamboo_home_dir.join("notification_preferences.json"),
129        ));
130        let mcp_manager = init_mcp_manager(config.clone());
131        let skill_manager = init_skill_manager(&data_dir).await;
132        let metrics_service = init_metrics_service(&data_dir).await?;
133
134        let startup_sessions = {
135            let entries = session_store.list_index_entries().await;
136            let mut sessions = Vec::new();
137            for entry in entries {
138                if let Some(session) = session_store
139                    .load_session(&entry.id)
140                    .await
141                    .map_err(AppError::StorageError)?
142                {
143                    sessions.push(session);
144                }
145            }
146            sessions
147        };
148        metrics_service
149            .reconcile_startup_sessions(startup_sessions, &[])
150            .await
151            .map_err(|error| {
152                AppError::InternalError(anyhow::anyhow!(
153                    "Failed to reconcile stale metrics state on startup: {error}"
154                ))
155            })?;
156
157        let agent_runners: Arc<RwLock<HashMap<String, AgentRunner>>> =
158            Arc::new(RwLock::new(HashMap::new()));
159        spawn_runner_cleanup_task(agent_runners.clone(), None);
160
161        let process_registry = Arc::new(ProcessRegistry::new());
162        let (provider_lock, provider_handle) = build_provider_handles(provider);
163
164        // Initialize multi-provider registry (for features.provider_model_ref).
165        let config_snapshot = config.read().await;
166        let provider_registry = match bamboo_llm::ProviderRegistry::from_config(
167            &config_snapshot,
168            bamboo_home_dir.clone(),
169        )
170        .await
171        {
172            Ok(registry) => Arc::new(registry),
173            Err(e) => {
174                tracing::error!("Failed to create provider registry: {}", e);
175                Arc::new(
176                    bamboo_llm::ProviderRegistry::from_config(
177                        &Config::default(),
178                        bamboo_home_dir.clone(),
179                    )
180                    .await
181                    .expect("Cannot create even an empty provider registry"),
182                )
183            }
184        };
185        drop(config_snapshot);
186
187        let provider_router = Arc::new(bamboo_llm::ProviderModelRouter::new(
188            provider_registry.clone(),
189        ));
190        let model_catalog = Arc::new(bamboo_llm::ModelCatalogService::new(
191            provider_registry.clone(),
192        ));
193
194        let base_tools = build_base_tools(
195            config.clone(),
196            permission_checker.clone(),
197            mcp_manager.clone(),
198            skill_manager.clone(),
199            storage.clone(),
200            persistence.clone(),
201            sessions.clone(),
202            bamboo_home_dir.clone(),
203        );
204
205        // Long-lived session event senders map (UI subscriptions + background tasks).
206        let session_event_senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>> =
207            Arc::new(RwLock::new(HashMap::new()));
208
209        // Account-scoped durable change feed. Opening the journal recovers the
210        // max seq so the sequence counter stays monotonic across restarts.
211        let account_sink = bamboo_engine::events::AccountEventSink::new(data_dir.join("events"))
212            .map_err(|e| {
213                AppError::InternalError(anyhow::anyhow!(
214                    "failed to initialize account change-feed journal: {e}"
215                ))
216            })?;
217
218        // Sub-agents are full agents with the full toolset (no per-role tool
219        // trimming): the child tool surface is the plain base tools.
220        let child_tools: Arc<dyn bamboo_agent_core::tools::ToolExecutor> = base_tools.clone();
221
222        // Unified agent runtime (shared resources for all execution paths).
223        // default_tools = base_tools (builtin + MCP + memory + skills) as a safe fallback.
224        // Interactive execution paths pass an explicit tool surface override:
225        // root sessions use ToolSurface::Root; child sessions use ToolSurface::Child.
226        let agent = Arc::new(
227            bamboo_engine::Agent::builder()
228                .storage(storage.clone())
229                .persistence(persistence.clone())
230                .attachment_reader(session_store.clone())
231                .skill_manager(skill_manager.clone())
232                .metrics_collector(metrics_service.collector())
233                .config(config.clone())
234                .provider(provider_handle.clone())
235                .default_tools(base_tools.clone())
236                .build()
237                .expect("agent runtime should be fully configured"),
238        );
239
240        let child_completion_coordinator =
241            Arc::new(bamboo_engine::ChildCompletionCoordinator::new(
242                storage.clone(),
243                persistence.clone(),
244                sessions.clone(),
245                agent_runners.clone(),
246                session_event_senders.clone(),
247                agent.clone(),
248                config.clone(),
249                provider_registry.clone(),
250                provider_router.clone(),
251                data_dir.clone(),
252                Some(account_sink.inbox()),
253            ));
254
255        // Initialize sub-session spawn scheduler (async background jobs).
256        let config_snapshot = config.read().await.clone();
257
258        // When a broker is configured, run the MCP proxy service: deployed
259        // workers forward their (host-bound) MCP tool calls here, and we execute
260        // them against this orchestrator's real MCP servers (single MCP host).
261        if let Some(broker) = config_snapshot.subagents.broker.clone() {
262            if !broker.endpoint.trim().is_empty() {
263                let backend: std::sync::Arc<dyn bamboo_agent_core::tools::ToolExecutor> =
264                    std::sync::Arc::new(bamboo_mcp::executor::McpToolExecutor::new(
265                        mcp_manager.clone(),
266                        mcp_manager.tool_index(),
267                    ));
268                tokio::spawn(async move {
269                    let me = bamboo_broker::AgentRef {
270                        session_id: bamboo_broker::ORCHESTRATOR_ID.to_string(),
271                        role: Some("orchestrator".into()),
272                    };
273                    if let Err(e) =
274                        bamboo_broker::serve_mcp_proxy(&broker.endpoint, me, &broker.token, backend)
275                            .await
276                    {
277                        tracing::warn!("MCP proxy service ended: {e}");
278                    }
279                });
280            }
281        }
282        let external_runner =
283            bamboo_engine::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        // Background memory "gardener": opt-in blob remediation + near-duplicate
325        // consolidation. No-op cost unless `memory.gardener_enabled` /
326        // `memory.dedup_gardener_enabled` is set; an empty prefilter makes zero LLM calls.
327        bamboo_engine::gardener::spawn_gardener_task(bamboo_engine::auto_dream::AutoDreamContext {
328            session_store: session_store.clone(),
329            storage: storage.clone(),
330            provider: provider_handle.clone(),
331            config: config.clone(),
332            provider_registry: provider_registry.clone(),
333        });
334
335        let config_for_resolver = config.clone();
336        let subagent_model_resolver: OptionalSubagentModelResolver = {
337            let registry = provider_registry.clone();
338            Some(Arc::new(
339                move |subagent_type: String| -> futures::future::BoxFuture<
340                    'static,
341                    Option<bamboo_domain::ProviderModelRef>,
342                > {
343                    let config_for_resolver = config_for_resolver.clone();
344                    let registry = registry.clone();
345                    Box::pin(async move {
346                        let config_snap = config_for_resolver.read().await.clone();
347                        bamboo_engine::model_config_helper::resolve_subagent_model_ref(
348                            &config_snap,
349                            &config_snap.provider,
350                            &registry,
351                            &subagent_type,
352                        )
353                    })
354                },
355            ))
356        };
357
358        let tools = build_root_tools(
359            tools_with_task.clone(),
360            schedule_store.clone(),
361            schedule_manager.clone(),
362            session_store.clone(),
363            storage.clone(),
364            persistence.clone(),
365            spawn_scheduler.clone(),
366            sessions.clone(),
367            agent_runners.clone(),
368            session_event_senders.clone(),
369            subagent_model_resolver,
370            config.clone(),
371            provider_registry.clone(),
372            config_snapshot.subagents.broker.clone(),
373        );
374
375        child_completion_coordinator
376            .set_root_tools(tools.clone())
377            .await;
378
379        let tool_factory =
380            crate::tools::ToolSurfaceFactory::new(base_tools, tools_with_task, tools);
381
382        let session_repo = bamboo_engine::SessionRepository::new(
383            sessions.clone(),
384            storage.clone(),
385            persistence.clone(),
386        );
387
388        // Dedicated child-session adapter backing the guardian review spawner.
389        // The guardian path passes an explicit model (no subagent_type routing)
390        // and registers its parent wait at the terminal gate (not via the
391        // adapter's coalescing slots), so a lightweight adapter with no resolver
392        // and a fresh wait-slot map suffices. `Arc<ChildSessionAdapter>` doubles
393        // as `Arc<dyn GuardianSpawner>`.
394        let child_adapter = Arc::new(crate::tools::ChildSessionAdapter {
395            session_store: session_store.clone(),
396            storage: storage.clone(),
397            persistence: persistence.clone(),
398            scheduler: spawn_scheduler.clone(),
399            sessions_cache: sessions.clone(),
400            agent_runners: agent_runners.clone(),
401            session_event_senders: session_event_senders.clone(),
402            subagent_model_resolver: None,
403            config: config.clone(),
404            parent_wait_slots: Arc::new(dashmap::DashMap::new()),
405        });
406        let guardian_spawner: Arc<dyn bamboo_engine::GuardianSpawner> = child_adapter.clone();
407        // Wire the spawner into the completion coordinator too, so a resumed run
408        // can re-spawn a guardian to re-review a fix after a reject verdict.
409        child_completion_coordinator
410            .set_guardian_spawner(guardian_spawner.clone())
411            .await;
412
413        // The completion coordinator doubles as the bash self-resume hook
414        // (issue #84 Phase 2b): it polls the live shell registry and resumes a
415        // session once all its background bash shells finish.
416        let bash_resume_hook: Arc<dyn bamboo_engine::BashResumeHook> =
417            child_completion_coordinator.clone();
418
419        Ok(Self {
420            app_data_dir: bamboo_home_dir,
421            config,
422            provider: provider_lock,
423            provider_handle,
424            sessions,
425            storage,
426            session_store,
427            session_repo,
428            persistence,
429            spawn_scheduler,
430            child_completion_coordinator,
431            guardian_spawner,
432            bash_resume_hook,
433            schedule_store,
434            schedule_manager,
435            tool_factory,
436            permission_checker,
437            notification_service,
438            cancel_tokens: Arc::new(RwLock::new(HashMap::new())),
439            skill_manager,
440            mcp_manager,
441            metrics_service,
442            agent_runners,
443            session_event_senders,
444            account_sink,
445            process_registry,
446            metrics_bus: None, // Will be set by server if needed
447            agent,
448            provider_registry,
449            provider_router,
450            model_catalog,
451            title_gen_in_flight: Arc::new(dashmap::DashSet::new()),
452        })
453    }
454}