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        let data_dir = bamboo_home_dir.clone();
111        let (session_store, storage) = init_storage(&data_dir).await?;
112        let persistence = Arc::new(LockedSessionStore::new(storage.clone()));
113
114        // In-memory session cache (shared across handlers and background jobs).
115        let sessions: bamboo_engine::SessionCache = Arc::new(dashmap::DashMap::new());
116
117        let config = Arc::new(RwLock::new(config));
118
119        // Wire the configured-default-workspace resolver into agent-core. This keeps
120        // the dependency arrow pointing down (agent-core owns only the slot; the
121        // server fills it). The closure reads the server's LIVE in-memory config —
122        // not a fresh disk-reading Config::new(), which would diverge from the live
123        // config and clobber the global env-var cache (#38). `try_read` never blocks
124        // (the resolver is called from sync code, so a blocking read could deadlock);
125        // on the rare write-lock contention it returns the last successfully-resolved
126        // path so a session never transiently falls back to the process cwd.
127        {
128            let config_for_workspace = config.clone();
129            let last_known: Arc<std::sync::Mutex<Option<PathBuf>>> =
130                Arc::new(std::sync::Mutex::new(None));
131            bamboo_agent_core::workspace_state::set_default_workspace_provider(Box::new(
132                move || match config_for_workspace.try_read() {
133                    Ok(cfg) => {
134                        let path = cfg.get_default_work_area_path();
135                        if let Ok(mut cache) = last_known.lock() {
136                            *cache = path.clone();
137                        }
138                        path
139                    }
140                    Err(_) => last_known.lock().ok().and_then(|c| c.clone()),
141                },
142            ));
143        }
144
145        let permission_checker = load_permission_checker(&bamboo_home_dir).await;
146        let notification_service = Arc::new(bamboo_notification::NotificationService::new(
147            bamboo_home_dir.join("notification_preferences.json"),
148        ));
149        let mcp_manager = init_mcp_manager(config.clone());
150        let skill_manager = init_skill_manager(&data_dir).await;
151        let metrics_service = init_metrics_service(&data_dir).await?;
152
153        let startup_sessions = {
154            let entries = session_store.list_index_entries().await;
155            let mut sessions = Vec::new();
156            for entry in entries {
157                if let Some(session) = session_store
158                    .load_session(&entry.id)
159                    .await
160                    .map_err(AppError::StorageError)?
161                {
162                    sessions.push(session);
163                }
164            }
165            sessions
166        };
167        metrics_service
168            .reconcile_startup_sessions(startup_sessions, &[])
169            .await
170            .map_err(|error| {
171                AppError::InternalError(anyhow::anyhow!(
172                    "Failed to reconcile stale metrics state on startup: {error}"
173                ))
174            })?;
175
176        let agent_runners: Arc<RwLock<HashMap<String, AgentRunner>>> =
177            Arc::new(RwLock::new(HashMap::new()));
178        spawn_runner_cleanup_task(agent_runners.clone(), None);
179
180        let process_registry = Arc::new(ProcessRegistry::new());
181        let (provider_lock, provider_handle) = build_provider_handles(provider);
182
183        // Initialize multi-provider registry (for features.provider_model_ref).
184        let config_snapshot = config.read().await;
185        let provider_registry = match bamboo_llm::ProviderRegistry::from_config(
186            &config_snapshot,
187            bamboo_home_dir.clone(),
188        )
189        .await
190        {
191            Ok(registry) => Arc::new(registry),
192            Err(e) => {
193                tracing::error!("Failed to create provider registry: {}", e);
194                Arc::new(
195                    bamboo_llm::ProviderRegistry::from_config(
196                        &Config::default(),
197                        bamboo_home_dir.clone(),
198                    )
199                    .await
200                    .expect("Cannot create even an empty provider registry"),
201                )
202            }
203        };
204        drop(config_snapshot);
205
206        let provider_router = Arc::new(bamboo_llm::ProviderModelRouter::new(
207            provider_registry.clone(),
208        ));
209        let model_catalog = Arc::new(bamboo_llm::ModelCatalogService::new(
210            provider_registry.clone(),
211        ));
212
213        let base_tools = build_base_tools(
214            config.clone(),
215            permission_checker.clone(),
216            mcp_manager.clone(),
217            skill_manager.clone(),
218            storage.clone(),
219            persistence.clone(),
220            sessions.clone(),
221            bamboo_home_dir.clone(),
222        );
223
224        // Long-lived session event senders map (UI subscriptions + background tasks).
225        let session_event_senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>> =
226            Arc::new(RwLock::new(HashMap::new()));
227
228        // Account-scoped durable change feed. Opening the journal recovers the
229        // max seq so the sequence counter stays monotonic across restarts.
230        let account_sink = bamboo_engine::events::AccountEventSink::new(data_dir.join("events"))
231            .map_err(|e| {
232                AppError::InternalError(anyhow::anyhow!(
233                    "failed to initialize account change-feed journal: {e}"
234                ))
235            })?;
236
237        // Sub-agents are full agents with the full toolset (no per-role tool
238        // trimming): the child tool surface is the plain base tools.
239        let child_tools: Arc<dyn bamboo_agent_core::tools::ToolExecutor> = base_tools.clone();
240
241        // Unified agent runtime (shared resources for all execution paths).
242        // default_tools = base_tools (builtin + MCP + memory + skills) as a safe fallback.
243        // Interactive execution paths pass an explicit tool surface override:
244        // root sessions use ToolSurface::Root; child sessions use ToolSurface::Child.
245        let agent = Arc::new(
246            bamboo_engine::Agent::builder()
247                .storage(storage.clone())
248                .persistence(persistence.clone())
249                .attachment_reader(session_store.clone())
250                .skill_manager(skill_manager.clone())
251                .metrics_collector(metrics_service.collector())
252                .config(config.clone())
253                .provider(provider_handle.clone())
254                .default_tools(base_tools.clone())
255                .build()
256                .expect("agent runtime should be fully configured"),
257        );
258
259        let child_completion_coordinator =
260            Arc::new(bamboo_engine::ChildCompletionCoordinator::new(
261                storage.clone(),
262                persistence.clone(),
263                sessions.clone(),
264                agent_runners.clone(),
265                session_event_senders.clone(),
266                agent.clone(),
267                config.clone(),
268                provider_registry.clone(),
269                provider_router.clone(),
270                data_dir.clone(),
271                Some(account_sink.inbox()),
272            ));
273
274        // Initialize sub-session spawn scheduler (async background jobs).
275        let config_snapshot = config.read().await.clone();
276
277        // When a broker is configured, run the MCP proxy service under a
278        // supervisor (issue #47): deployed workers forward their (host-bound)
279        // MCP tool calls here, and we execute them against this orchestrator's
280        // real MCP servers (single MCP host). The supervisor restarts the proxy
281        // with bounded backoff after a transient WebSocket drop instead of
282        // permanently disabling proxied tools for the worker's lifetime.
283        let mcp_proxy_shutdown = tokio_util::sync::CancellationToken::new();
284        if let Some(broker) = config_snapshot.subagents.broker.clone() {
285            if !broker.endpoint.trim().is_empty() {
286                let backend: std::sync::Arc<dyn bamboo_agent_core::tools::ToolExecutor> =
287                    std::sync::Arc::new(bamboo_mcp::executor::McpToolExecutor::new(
288                        mcp_manager.clone(),
289                        mcp_manager.tool_index(),
290                    ));
291                let shutdown = mcp_proxy_shutdown.clone();
292                tokio::spawn(async move {
293                    let me = bamboo_broker::AgentRef {
294                        session_id: bamboo_broker::ORCHESTRATOR_ID.to_string(),
295                        role: Some("orchestrator".into()),
296                    };
297                    // No per-role tool allowlist is configured here yet, so the
298                    // proxy is unrestricted (every role sees/can call all
299                    // host-bound tools) — identical to the pre-#54 behavior.
300                    // Wiring a config-driven allowlist in is a follow-up; the
301                    // filtering mechanism (RoleToolAllowlist) is now in place.
302                    let allowlist =
303                        std::sync::Arc::new(bamboo_broker::RoleToolAllowlist::unrestricted());
304                    bamboo_broker::serve_mcp_proxy_supervised(
305                        &broker.endpoint,
306                        me,
307                        &broker.token,
308                        backend,
309                        allowlist,
310                        shutdown,
311                    )
312                    .await;
313                });
314            }
315        }
316        let external_runner =
317            bamboo_engine::external_agents::runtime::build_external_child_runner(&config_snapshot);
318        let spawn_scheduler = build_spawn_scheduler(
319            agent.clone(),
320            child_tools,
321            sessions.clone(),
322            agent_runners.clone(),
323            session_event_senders.clone(),
324            external_runner,
325            Some(provider_router.clone()),
326            Some(child_completion_coordinator.clone()),
327            Some(data_dir.clone()),
328            Some(account_sink.inbox()),
329        );
330
331        let tools_with_task = base_tools.clone();
332
333        let schedule_store = init_schedule_store(&data_dir).await?;
334        let schedule_manager = build_schedule_manager(
335            schedule_store.clone(),
336            agent.clone(),
337            tools_with_task.clone(),
338            sessions.clone(),
339            agent_runners.clone(),
340            session_event_senders.clone(),
341            persistence.clone(),
342            config.clone(),
343            provider_registry.clone(),
344            Some(data_dir.clone()),
345            Some(account_sink.inbox()),
346        );
347
348        bamboo_engine::auto_dream::spawn_auto_dream_task(
349            bamboo_engine::auto_dream::AutoDreamContext {
350                session_store: session_store.clone(),
351                storage: storage.clone(),
352                provider: provider_handle.clone(),
353                config: config.clone(),
354                provider_registry: provider_registry.clone(),
355            },
356        );
357
358        // Background memory "gardener": opt-in blob remediation + near-duplicate
359        // consolidation. No-op cost unless `memory.gardener_enabled` /
360        // `memory.dedup_gardener_enabled` is set; an empty prefilter makes zero LLM calls.
361        bamboo_engine::gardener::spawn_gardener_task(bamboo_engine::auto_dream::AutoDreamContext {
362            session_store: session_store.clone(),
363            storage: storage.clone(),
364            provider: provider_handle.clone(),
365            config: config.clone(),
366            provider_registry: provider_registry.clone(),
367        });
368
369        let config_for_resolver = config.clone();
370        let subagent_model_resolver: OptionalSubagentModelResolver = {
371            let registry = provider_registry.clone();
372            Some(Arc::new(
373                move |subagent_type: String| -> futures::future::BoxFuture<
374                    'static,
375                    Option<bamboo_domain::ProviderModelRef>,
376                > {
377                    let config_for_resolver = config_for_resolver.clone();
378                    let registry = registry.clone();
379                    Box::pin(async move {
380                        let config_snap = config_for_resolver.read().await.clone();
381                        bamboo_engine::model_config_helper::resolve_subagent_model_ref(
382                            &config_snap,
383                            &config_snap.provider,
384                            &registry,
385                            &subagent_type,
386                        )
387                    })
388                },
389            ))
390        };
391
392        let tools = build_root_tools(
393            tools_with_task.clone(),
394            schedule_store.clone(),
395            schedule_manager.clone(),
396            session_store.clone(),
397            storage.clone(),
398            persistence.clone(),
399            spawn_scheduler.clone(),
400            sessions.clone(),
401            agent_runners.clone(),
402            session_event_senders.clone(),
403            subagent_model_resolver,
404            config.clone(),
405            provider_registry.clone(),
406            config_snapshot.subagents.broker.clone(),
407        );
408
409        child_completion_coordinator
410            .set_root_tools(tools.clone())
411            .await;
412
413        let tool_factory =
414            crate::tools::ToolSurfaceFactory::new(base_tools, tools_with_task, tools);
415
416        let session_repo = bamboo_engine::SessionRepository::new(
417            sessions.clone(),
418            storage.clone(),
419            persistence.clone(),
420        );
421
422        // Dedicated child-session adapter backing the guardian review spawner.
423        // The guardian path passes an explicit model (no subagent_type routing)
424        // and registers its parent wait at the terminal gate (not via the
425        // adapter's coalescing slots), so a lightweight adapter with no resolver
426        // and a fresh wait-slot map suffices. `Arc<ChildSessionAdapter>` doubles
427        // as `Arc<dyn GuardianSpawner>`.
428        let child_adapter = Arc::new(crate::tools::ChildSessionAdapter {
429            session_store: session_store.clone(),
430            storage: storage.clone(),
431            persistence: persistence.clone(),
432            scheduler: spawn_scheduler.clone(),
433            sessions_cache: sessions.clone(),
434            agent_runners: agent_runners.clone(),
435            session_event_senders: session_event_senders.clone(),
436            subagent_model_resolver: None,
437            config: config.clone(),
438            parent_wait_slots: Arc::new(dashmap::DashMap::new()),
439        });
440        let guardian_spawner: Arc<dyn bamboo_engine::GuardianSpawner> = child_adapter.clone();
441        // Wire the spawner into the completion coordinator too, so a resumed run
442        // can re-spawn a guardian to re-review a fix after a reject verdict.
443        child_completion_coordinator
444            .set_guardian_spawner(guardian_spawner.clone())
445            .await;
446
447        // The completion coordinator doubles as the bash self-resume hook
448        // (issue #84 Phase 2b): it polls the live shell registry and resumes a
449        // session once all its background bash shells finish.
450        let bash_resume_hook: Arc<dyn bamboo_engine::BashResumeHook> =
451            child_completion_coordinator.clone();
452
453        Ok(Self {
454            app_data_dir: bamboo_home_dir,
455            config,
456            config_io_lock: Arc::new(tokio::sync::Mutex::new(())),
457            provider: provider_lock,
458            provider_handle,
459            sessions,
460            storage,
461            session_store,
462            session_repo,
463            persistence,
464            spawn_scheduler,
465            child_completion_coordinator,
466            guardian_spawner,
467            bash_resume_hook,
468            schedule_store,
469            schedule_manager,
470            tool_factory,
471            permission_checker,
472            notification_service,
473            cancel_tokens: Arc::new(RwLock::new(HashMap::new())),
474            mcp_proxy_shutdown,
475            skill_manager,
476            mcp_manager,
477            metrics_service,
478            agent_runners,
479            session_event_senders,
480            account_sink,
481            process_registry,
482            metrics_bus: None, // Will be set by server if needed
483            agent,
484            provider_registry,
485            provider_router,
486            model_catalog,
487            title_gen_in_flight: Arc::new(dashmap::DashSet::new()),
488        })
489    }
490}