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_session_map_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 pub async fn new(bamboo_home_dir: PathBuf) -> Result<Self, AppError> {
47 bamboo_config::paths::init_bamboo_dir(bamboo_home_dir.clone());
50
51 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 pub async fn new_with_provider(
105 bamboo_home_dir: PathBuf,
106 config: Config,
107 provider: Arc<dyn LLMProvider>,
108 ) -> Result<Self, AppError> {
109 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 let sessions: bamboo_engine::SessionCache = Arc::new(dashmap::DashMap::new());
116
117 let mut config = config;
124 let embedded_broker = maybe_embed_broker(&mut config, &data_dir).await;
125
126 let config = Arc::new(RwLock::new(config));
127
128 {
137 let config_for_workspace = config.clone();
138 let last_known: Arc<std::sync::Mutex<Option<PathBuf>>> =
139 Arc::new(std::sync::Mutex::new(None));
140 bamboo_agent_core::workspace_state::set_default_workspace_provider(Box::new(
141 move || match config_for_workspace.try_read() {
142 Ok(cfg) => {
143 let path = cfg.get_default_work_area_path();
144 if let Ok(mut cache) = last_known.lock() {
145 *cache = path.clone();
146 }
147 path
148 }
149 Err(_) => last_known.lock().ok().and_then(|c| c.clone()),
150 },
151 ));
152 }
153
154 let permission_checker = load_permission_checker(&bamboo_home_dir).await;
155 let notification_service = Arc::new(bamboo_notification::NotificationService::new(
156 bamboo_home_dir.join("notification_preferences.json"),
157 ));
158 let mcp_manager = init_mcp_manager(config.clone());
159 let skill_manager = init_skill_manager(&data_dir).await;
160 let metrics_service = init_metrics_service(&data_dir).await?;
161
162 let startup_sessions = {
163 let entries = session_store.list_index_entries().await;
164 let mut sessions = Vec::new();
165 for entry in entries {
166 if let Some(session) = session_store
167 .load_session(&entry.id)
168 .await
169 .map_err(AppError::StorageError)?
170 {
171 sessions.push(session);
172 }
173 }
174 sessions
175 };
176 metrics_service
177 .reconcile_startup_sessions(startup_sessions, &[])
178 .await
179 .map_err(|error| {
180 AppError::InternalError(anyhow::anyhow!(
181 "Failed to reconcile stale metrics state on startup: {error}"
182 ))
183 })?;
184
185 let agent_runners: Arc<RwLock<HashMap<String, AgentRunner>>> =
186 Arc::new(RwLock::new(HashMap::new()));
187 let process_registry = Arc::new(ProcessRegistry::new());
192 let (provider_lock, provider_handle) = build_provider_handles(provider);
193
194 let config_snapshot = config.read().await;
196 let provider_registry = match bamboo_llm::ProviderRegistry::from_config(
197 &config_snapshot,
198 bamboo_home_dir.clone(),
199 )
200 .await
201 {
202 Ok(registry) => Arc::new(registry),
203 Err(e) => {
204 tracing::error!("Failed to create provider registry: {}", e);
205 Arc::new(
206 bamboo_llm::ProviderRegistry::from_config(
207 &Config::default(),
208 bamboo_home_dir.clone(),
209 )
210 .await
211 .expect("Cannot create even an empty provider registry"),
212 )
213 }
214 };
215 drop(config_snapshot);
216
217 let provider_router = Arc::new(bamboo_llm::ProviderModelRouter::new(
218 provider_registry.clone(),
219 ));
220 let model_catalog = Arc::new(bamboo_llm::ModelCatalogService::new(
221 provider_registry.clone(),
222 ));
223
224 let base_tools = build_base_tools(
225 config.clone(),
226 permission_checker.clone(),
227 mcp_manager.clone(),
228 skill_manager.clone(),
229 storage.clone(),
230 persistence.clone(),
231 sessions.clone(),
232 bamboo_home_dir.clone(),
233 );
234
235 let session_event_senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>> =
237 Arc::new(RwLock::new(HashMap::new()));
238
239 spawn_session_map_cleanup_task(agent_runners.clone(), session_event_senders.clone(), None);
243
244 let account_sink = bamboo_engine::events::AccountEventSink::new(data_dir.join("events"))
247 .map_err(|e| {
248 AppError::InternalError(anyhow::anyhow!(
249 "failed to initialize account change-feed journal: {e}"
250 ))
251 })?;
252
253 let child_tools: Arc<dyn bamboo_agent_core::tools::ToolExecutor> = base_tools.clone();
256
257 let agent = Arc::new(
262 bamboo_engine::Agent::builder()
263 .storage(storage.clone())
264 .persistence(persistence.clone())
265 .attachment_reader(session_store.clone())
266 .skill_manager(skill_manager.clone())
267 .metrics_collector(metrics_service.collector())
268 .config(config.clone())
269 .provider(provider_handle.clone())
270 .default_tools(base_tools.clone())
271 .build()
272 .expect("agent runtime should be fully configured"),
273 );
274
275 let child_completion_coordinator =
276 Arc::new(bamboo_engine::ChildCompletionCoordinator::new(
277 storage.clone(),
278 persistence.clone(),
279 sessions.clone(),
280 agent_runners.clone(),
281 session_event_senders.clone(),
282 agent.clone(),
283 config.clone(),
284 provider_registry.clone(),
285 provider_router.clone(),
286 data_dir.clone(),
287 Some(account_sink.inbox()),
288 ));
289
290 let config_snapshot = config.read().await.clone();
292
293 let mcp_proxy_shutdown = tokio_util::sync::CancellationToken::new();
300 if let Some(broker) = config_snapshot.subagents.broker.clone() {
301 if !broker.endpoint.trim().is_empty() {
302 let backend: std::sync::Arc<dyn bamboo_agent_core::tools::ToolExecutor> =
303 std::sync::Arc::new(bamboo_mcp::executor::McpToolExecutor::new(
304 mcp_manager.clone(),
305 mcp_manager.tool_index(),
306 ));
307 let shutdown = mcp_proxy_shutdown.clone();
308 tokio::spawn(async move {
309 let me = bamboo_broker::AgentRef {
310 session_id: bamboo_broker::ORCHESTRATOR_ID.to_string(),
311 role: Some("orchestrator".into()),
312 };
313 let allowlist =
319 std::sync::Arc::new(bamboo_broker::RoleToolAllowlist::unrestricted());
320 bamboo_broker::serve_mcp_proxy_supervised(
321 &broker.endpoint,
322 me,
323 &broker.token,
324 backend,
325 allowlist,
326 shutdown,
327 )
328 .await;
329 });
330 }
331 }
332 let external_runner =
333 bamboo_engine::external_agents::runtime::build_external_child_runner(&config_snapshot);
334 let spawn_scheduler = build_spawn_scheduler(
335 agent.clone(),
336 child_tools,
337 sessions.clone(),
338 agent_runners.clone(),
339 session_event_senders.clone(),
340 external_runner,
341 Some(provider_router.clone()),
342 Some(child_completion_coordinator.clone()),
343 Some(data_dir.clone()),
344 Some(account_sink.inbox()),
345 );
346
347 let tools_with_task = base_tools.clone();
348
349 let schedule_store = init_schedule_store(&data_dir).await?;
350 let schedule_manager = build_schedule_manager(
351 schedule_store.clone(),
352 agent.clone(),
353 tools_with_task.clone(),
354 sessions.clone(),
355 agent_runners.clone(),
356 session_event_senders.clone(),
357 persistence.clone(),
358 config.clone(),
359 provider_registry.clone(),
360 Some(data_dir.clone()),
361 Some(account_sink.inbox()),
362 );
363
364 bamboo_engine::auto_dream::spawn_auto_dream_task(
365 bamboo_engine::auto_dream::AutoDreamContext {
366 session_store: session_store.clone(),
367 storage: storage.clone(),
368 provider: provider_handle.clone(),
369 config: config.clone(),
370 provider_registry: provider_registry.clone(),
371 },
372 );
373
374 bamboo_engine::gardener::spawn_gardener_task(bamboo_engine::auto_dream::AutoDreamContext {
378 session_store: session_store.clone(),
379 storage: storage.clone(),
380 provider: provider_handle.clone(),
381 config: config.clone(),
382 provider_registry: provider_registry.clone(),
383 });
384
385 let config_for_resolver = config.clone();
386 let subagent_model_resolver: OptionalSubagentModelResolver = {
387 let registry = provider_registry.clone();
388 Some(Arc::new(
389 move |subagent_type: String| -> futures::future::BoxFuture<
390 'static,
391 Option<bamboo_domain::ProviderModelRef>,
392 > {
393 let config_for_resolver = config_for_resolver.clone();
394 let registry = registry.clone();
395 Box::pin(async move {
396 let config_snap = config_for_resolver.read().await.clone();
397 bamboo_engine::model_config_helper::resolve_subagent_model_ref(
398 &config_snap,
399 &config_snap.provider,
400 ®istry,
401 &subagent_type,
402 )
403 })
404 },
405 ))
406 };
407
408 let config_io_lock = Arc::new(tokio::sync::Mutex::new(()));
412 let fabric_registry: crate::tools::DeployedRegistry =
413 Arc::new(tokio::sync::Mutex::new(HashMap::new()));
414 let fabric_bamboo_bin =
415 std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("bamboo"));
416 let fabric_deployer = Arc::new(bamboo_server_tools::FabricDeployer::new(
417 config.clone(),
418 config_io_lock.clone(),
419 bamboo_home_dir.clone(),
420 fabric_registry,
421 fabric_bamboo_bin,
422 ));
423 let health_monitor = fabric_deployer
428 .clone()
429 .spawn_health_monitor()
430 .await
431 .map(HealthMonitor);
432
433 let tools = build_root_tools(
434 tools_with_task.clone(),
435 schedule_store.clone(),
436 schedule_manager.clone(),
437 session_store.clone(),
438 storage.clone(),
439 persistence.clone(),
440 spawn_scheduler.clone(),
441 sessions.clone(),
442 agent_runners.clone(),
443 session_event_senders.clone(),
444 subagent_model_resolver,
445 config.clone(),
446 provider_registry.clone(),
447 config_snapshot.subagents.broker.clone(),
448 fabric_deployer.clone(),
449 );
450
451 child_completion_coordinator
452 .set_root_tools(tools.clone())
453 .await;
454
455 let tool_factory =
456 crate::tools::ToolSurfaceFactory::new(base_tools, tools_with_task, tools);
457
458 let session_repo = bamboo_engine::SessionRepository::new(
459 sessions.clone(),
460 storage.clone(),
461 persistence.clone(),
462 );
463
464 let child_adapter = Arc::new(crate::tools::ChildSessionAdapter {
471 session_store: session_store.clone(),
472 storage: storage.clone(),
473 persistence: persistence.clone(),
474 scheduler: spawn_scheduler.clone(),
475 sessions_cache: sessions.clone(),
476 agent_runners: agent_runners.clone(),
477 session_event_senders: session_event_senders.clone(),
478 subagent_model_resolver: None,
479 config: config.clone(),
480 parent_wait_slots: Arc::new(dashmap::DashMap::new()),
481 });
482 let guardian_spawner: Arc<dyn bamboo_engine::GuardianSpawner> = child_adapter.clone();
483 child_completion_coordinator
486 .set_guardian_spawner(guardian_spawner.clone())
487 .await;
488
489 let bash_resume_hook: Arc<dyn bamboo_engine::BashResumeHook> =
493 child_completion_coordinator.clone();
494
495 reconcile_fabric_on_boot(&config, &bamboo_home_dir).await;
500
501 Ok(Self {
502 app_data_dir: bamboo_home_dir,
503 config,
504 config_io_lock,
505 fabric_deployer,
506 embedded_broker,
507 health_monitor,
508 provider: provider_lock,
509 provider_handle,
510 sessions,
511 storage,
512 session_store,
513 session_repo,
514 persistence,
515 spawn_scheduler,
516 child_completion_coordinator,
517 guardian_spawner,
518 bash_resume_hook,
519 schedule_store,
520 schedule_manager,
521 tool_factory,
522 permission_checker,
523 notification_service,
524 cancel_tokens: Arc::new(RwLock::new(HashMap::new())),
525 mcp_proxy_shutdown,
526 skill_manager,
527 mcp_manager,
528 metrics_service,
529 agent_runners,
530 session_event_senders,
531 account_sink,
532 process_registry,
533 metrics_bus: None, agent,
535 provider_registry,
536 provider_router,
537 model_catalog,
538 title_gen_in_flight: Arc::new(dashmap::DashSet::new()),
539 pairing_codes: Arc::new(dashmap::DashMap::new()),
540 pairing_code_guard: Arc::new(crate::handlers::settings::PairingCodeGuard::default()),
541 root_password_guard: Arc::new(crate::handlers::settings::RootPasswordGuard::default()),
542 })
544 }
545}
546
547pub struct EmbeddedBroker {
550 task: tokio::task::JoinHandle<()>,
551 gc_task: tokio::task::JoinHandle<()>,
552}
553
554impl Drop for EmbeddedBroker {
555 fn drop(&mut self) {
556 self.task.abort();
557 self.gc_task.abort();
558 }
559}
560
561pub struct HealthMonitor(tokio::task::JoinHandle<()>);
565
566impl Drop for HealthMonitor {
567 fn drop(&mut self) {
568 self.0.abort();
569 }
570}
571
572async fn maybe_embed_broker(
584 config: &mut bamboo_llm::Config,
585 data_dir: &std::path::Path,
586) -> Option<EmbeddedBroker> {
587 if let Some(external) = load_external_broker(data_dir) {
592 let endpoint = external.endpoint.trim().to_string();
593 if broker_endpoint_reachable(&endpoint).await {
594 tracing::info!(%endpoint, "using external broker from broker.json");
595 config.subagents.broker = Some(external);
596 return None;
597 }
598 tracing::warn!(
599 %endpoint,
600 "broker.json endpoint is unreachable — embedding a fresh in-process broker instead"
601 );
602 }
603
604 let listener = match tokio::net::TcpListener::bind("127.0.0.1:0").await {
605 Ok(l) => l,
606 Err(e) => {
607 tracing::warn!("embedded broker: bind failed, sub-agent dispatch disabled: {e}");
608 return None;
609 }
610 };
611 let port = match listener.local_addr() {
612 Ok(a) => a.port(),
613 Err(e) => {
614 tracing::warn!("embedded broker: local_addr failed: {e}");
615 return None;
616 }
617 };
618 let token = uuid::Uuid::new_v4().simple().to_string();
619 let root = data_dir.join("broker");
620 let core = Arc::new(bamboo_broker::BrokerCore::new(root));
621 let gc_task = core
624 .clone()
625 .spawn_mailbox_gc(std::time::Duration::from_secs(300));
626 let server = Arc::new(bamboo_broker::BrokerServer::new(core, token.clone()));
627
628 let task = tokio::spawn(async move {
629 if let Err(e) = server.serve(listener).await {
630 tracing::error!("embedded broker serve loop ended: {e}");
631 }
632 });
633
634 config.subagents.broker = Some(bamboo_config::BrokerClientConfig {
637 endpoint: format!("ws://127.0.0.1:{port}"),
638 token,
639 token_encrypted: None,
640 });
641 tracing::info!(port, "embedded mailbox bus (broker) started in-process");
642 Some(EmbeddedBroker { task, gc_task })
643}
644
645fn load_external_broker(data_dir: &std::path::Path) -> Option<bamboo_config::BrokerClientConfig> {
654 let path = data_dir.join("broker.json");
655 let bytes = std::fs::read(&path).ok()?;
656 match serde_json::from_slice::<bamboo_config::BrokerClientConfig>(&bytes) {
657 Ok(cfg) if !cfg.endpoint.trim().is_empty() => Some(cfg),
658 Ok(_) => {
659 tracing::warn!(?path, "broker.json has an empty endpoint — ignoring");
660 None
661 }
662 Err(e) => {
663 tracing::warn!(?path, "broker.json present but unparseable: {e}");
664 None
665 }
666 }
667}
668
669async fn broker_endpoint_reachable(endpoint: &str) -> bool {
674 let host_port = endpoint
675 .trim()
676 .trim_start_matches("wss://")
677 .trim_start_matches("ws://")
678 .split('/')
679 .next()
680 .unwrap_or("");
681 if host_port.is_empty() {
682 return false;
683 }
684 matches!(
685 tokio::time::timeout(
686 std::time::Duration::from_millis(500),
687 tokio::net::TcpStream::connect(host_port),
688 )
689 .await,
690 Ok(Ok(_))
691 )
692}
693
694async fn reconcile_fabric_on_boot(
699 config: &Arc<RwLock<bamboo_llm::Config>>,
700 data_dir: &std::path::Path,
701) {
702 use bamboo_config::cluster_fabric::NodeStatus;
703
704 let snapshot = {
705 let mut cfg = config.write().await;
706 let mut changed = 0usize;
707 for node in &mut cfg.cluster_fabric.nodes {
708 if let Some(state) = node.state.as_mut() {
709 if matches!(state.status, NodeStatus::Running | NodeStatus::Deploying) {
710 state.status = NodeStatus::Unreachable;
711 state.last_error =
712 Some("orchestrator restarted; worker no longer tracked".to_string());
713 changed += 1;
714 }
715 }
716 }
717 if changed == 0 {
718 return;
719 }
720 tracing::info!(
721 reconciled = changed,
722 "cluster-fabric: marked stale Running nodes Unreachable on boot"
723 );
724 cfg.clone()
725 };
726
727 if let Err(e) = snapshot.save_to_dir(data_dir.to_path_buf()) {
728 tracing::warn!("cluster-fabric boot reconcile: failed to persist: {e}");
729 }
730}
731
732#[cfg(test)]
733mod broker_embed_tests {
734 use super::{broker_endpoint_reachable, load_external_broker};
735
736 #[test]
737 fn load_external_broker_reads_broker_json_not_config() {
738 let dir = tempfile::tempdir().unwrap();
739 assert!(load_external_broker(dir.path()).is_none());
741
742 std::fs::write(
744 dir.path().join("broker.json"),
745 r#"{ "endpoint": "wss://broker.example:9600", "token": "t" }"#,
746 )
747 .unwrap();
748 let got = load_external_broker(dir.path()).expect("parsed");
749 assert_eq!(got.endpoint, "wss://broker.example:9600");
750 assert_eq!(got.token, "t");
751
752 std::fs::write(dir.path().join("broker.json"), r#"{ "endpoint": " " }"#).unwrap();
754 assert!(load_external_broker(dir.path()).is_none());
755
756 std::fs::write(dir.path().join("broker.json"), "not json").unwrap();
758 assert!(load_external_broker(dir.path()).is_none());
759 }
760
761 #[tokio::test]
762 async fn reachability_probe_distinguishes_live_from_dead() {
763 let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
766 let dead = l.local_addr().unwrap();
767 drop(l);
768 assert!(!broker_endpoint_reachable(&format!("ws://{dead}")).await);
769
770 assert!(!broker_endpoint_reachable("").await);
772 assert!(!broker_endpoint_reachable("ws://").await);
773
774 let live = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
776 let addr = live.local_addr().unwrap();
777 assert!(broker_endpoint_reachable(&format!("ws://{addr}/stream")).await);
778 }
779}