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 session_watchers = super::watchers::SessionWatchers::new();
159 let mcp_manager = init_mcp_manager(config.clone());
160 let skill_manager = init_skill_manager(&data_dir).await;
161 let metrics_service = init_metrics_service(&data_dir).await?;
162
163 let startup_sessions = {
164 let entries = session_store.list_index_entries().await;
165 let mut sessions = Vec::new();
166 for entry in entries {
167 if let Some(session) = session_store
168 .load_session(&entry.id)
169 .await
170 .map_err(AppError::StorageError)?
171 {
172 sessions.push(session);
173 }
174 }
175 sessions
176 };
177 metrics_service
178 .reconcile_startup_sessions(startup_sessions, &[])
179 .await
180 .map_err(|error| {
181 AppError::InternalError(anyhow::anyhow!(
182 "Failed to reconcile stale metrics state on startup: {error}"
183 ))
184 })?;
185
186 let agent_runners: Arc<RwLock<HashMap<String, AgentRunner>>> =
187 Arc::new(RwLock::new(HashMap::new()));
188 let process_registry = Arc::new(ProcessRegistry::new());
193 let (provider_lock, provider_handle) = build_provider_handles(provider);
194
195 let config_snapshot = config.read().await;
197 let provider_registry = match bamboo_llm::ProviderRegistry::from_config(
198 &config_snapshot,
199 bamboo_home_dir.clone(),
200 )
201 .await
202 {
203 Ok(registry) => Arc::new(registry),
204 Err(e) => {
205 tracing::error!("Failed to create provider registry: {}", e);
206 Arc::new(
207 bamboo_llm::ProviderRegistry::from_config(
208 &Config::default(),
209 bamboo_home_dir.clone(),
210 )
211 .await
212 .expect("Cannot create even an empty provider registry"),
213 )
214 }
215 };
216 drop(config_snapshot);
217
218 let provider_router = Arc::new(bamboo_llm::ProviderModelRouter::new(
219 provider_registry.clone(),
220 ));
221 let model_catalog = Arc::new(bamboo_llm::ModelCatalogService::new(
222 provider_registry.clone(),
223 ));
224
225 let session_event_senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>> =
230 Arc::new(RwLock::new(HashMap::new()));
231
232 let notification_relay_deps = crate::app_state::session_events::NotificationRelayDeps {
238 notification_service: notification_service.clone(),
239 session_event_senders: session_event_senders.clone(),
240 session_watchers: session_watchers.clone(),
241 config: config.clone(),
242 };
243
244 let base_tools = build_base_tools(
245 config.clone(),
246 permission_checker.clone(),
247 mcp_manager.clone(),
248 skill_manager.clone(),
249 storage.clone(),
250 persistence.clone(),
251 sessions.clone(),
252 bamboo_home_dir.clone(),
253 notification_service.clone(),
254 session_event_senders.clone(),
255 session_watchers.clone(),
256 );
257
258 spawn_session_map_cleanup_task(agent_runners.clone(), session_event_senders.clone(), None);
262
263 let account_sink = bamboo_engine::events::AccountEventSink::new(data_dir.join("events"))
266 .map_err(|e| {
267 AppError::InternalError(anyhow::anyhow!(
268 "failed to initialize account change-feed journal: {e}"
269 ))
270 })?;
271
272 let child_tools: Arc<dyn bamboo_agent_core::tools::ToolExecutor> = base_tools.clone();
275
276 let agent = Arc::new(
281 bamboo_engine::Agent::builder()
282 .storage(storage.clone())
283 .persistence(persistence.clone())
284 .attachment_reader(session_store.clone())
285 .skill_manager(skill_manager.clone())
286 .metrics_collector(metrics_service.collector())
287 .config(config.clone())
288 .provider(provider_handle.clone())
289 .default_tools(base_tools.clone())
290 .build()
291 .expect("agent runtime should be fully configured"),
292 );
293
294 let child_completion_coordinator =
295 Arc::new(bamboo_engine::ChildCompletionCoordinator::new(
296 storage.clone(),
297 persistence.clone(),
298 sessions.clone(),
299 agent_runners.clone(),
300 session_event_senders.clone(),
301 agent.clone(),
302 config.clone(),
303 provider_registry.clone(),
304 provider_router.clone(),
305 data_dir.clone(),
306 Some(account_sink.inbox()),
307 ));
308
309 let config_snapshot = config.read().await.clone();
311
312 let mcp_proxy_shutdown = tokio_util::sync::CancellationToken::new();
319 if let Some(broker) = config_snapshot.subagents.broker.clone() {
320 if !broker.endpoint.trim().is_empty() {
321 let backend: std::sync::Arc<dyn bamboo_agent_core::tools::ToolExecutor> =
322 std::sync::Arc::new(bamboo_mcp::executor::McpToolExecutor::new(
323 mcp_manager.clone(),
324 mcp_manager.tool_index(),
325 ));
326 let shutdown = mcp_proxy_shutdown.clone();
327 tokio::spawn(async move {
328 let me = bamboo_broker::AgentRef {
329 session_id: bamboo_broker::ORCHESTRATOR_ID.to_string(),
330 role: Some("orchestrator".into()),
331 };
332 let allowlist =
338 std::sync::Arc::new(bamboo_broker::RoleToolAllowlist::unrestricted());
339 bamboo_broker::serve_mcp_proxy_supervised(
340 &broker.endpoint,
341 me,
342 &broker.token,
343 backend,
344 allowlist,
345 shutdown,
346 )
347 .await;
348 });
349 }
350 }
351 let external_runner =
352 bamboo_engine::external_agents::runtime::build_external_child_runner(&config_snapshot);
353 let spawn_scheduler = build_spawn_scheduler(
354 agent.clone(),
355 child_tools,
356 sessions.clone(),
357 agent_runners.clone(),
358 session_event_senders.clone(),
359 external_runner,
360 Some(provider_router.clone()),
361 Some(child_completion_coordinator.clone()),
362 Some(data_dir.clone()),
363 Some(account_sink.inbox()),
364 );
365
366 let tools_with_task = base_tools.clone();
367
368 let schedule_store = init_schedule_store(&data_dir).await?;
369 let schedule_manager = build_schedule_manager(
370 schedule_store.clone(),
371 agent.clone(),
372 tools_with_task.clone(),
373 sessions.clone(),
374 agent_runners.clone(),
375 session_event_senders.clone(),
376 persistence.clone(),
377 config.clone(),
378 provider_registry.clone(),
379 Some(data_dir.clone()),
380 Some(account_sink.inbox()),
381 notification_relay_deps.clone(),
382 );
383
384 bamboo_engine::auto_dream::spawn_auto_dream_task(
385 bamboo_engine::auto_dream::AutoDreamContext {
386 session_store: session_store.clone(),
387 storage: storage.clone(),
388 provider: provider_handle.clone(),
389 config: config.clone(),
390 provider_registry: provider_registry.clone(),
391 },
392 );
393
394 bamboo_engine::gardener::spawn_gardener_task(bamboo_engine::auto_dream::AutoDreamContext {
398 session_store: session_store.clone(),
399 storage: storage.clone(),
400 provider: provider_handle.clone(),
401 config: config.clone(),
402 provider_registry: provider_registry.clone(),
403 });
404
405 let config_for_resolver = config.clone();
406 let subagent_model_resolver: OptionalSubagentModelResolver = {
407 let registry = provider_registry.clone();
408 Some(Arc::new(
409 move |subagent_type: String| -> futures::future::BoxFuture<
410 'static,
411 Option<bamboo_domain::ProviderModelRef>,
412 > {
413 let config_for_resolver = config_for_resolver.clone();
414 let registry = registry.clone();
415 Box::pin(async move {
416 let config_snap = config_for_resolver.read().await.clone();
417 bamboo_engine::model_config_helper::resolve_subagent_model_ref(
418 &config_snap,
419 &config_snap.provider,
420 ®istry,
421 &subagent_type,
422 )
423 })
424 },
425 ))
426 };
427
428 let config_io_lock = Arc::new(tokio::sync::Mutex::new(()));
432 let fabric_registry: crate::tools::DeployedRegistry =
433 Arc::new(tokio::sync::Mutex::new(HashMap::new()));
434 let fabric_bamboo_bin =
435 std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("bamboo"));
436 let fabric_deployer = Arc::new(bamboo_server_tools::FabricDeployer::new(
437 config.clone(),
438 config_io_lock.clone(),
439 bamboo_home_dir.clone(),
440 fabric_registry,
441 fabric_bamboo_bin,
442 ));
443 let health_monitor = fabric_deployer
448 .clone()
449 .spawn_health_monitor()
450 .await
451 .map(HealthMonitor);
452
453 let tools = build_root_tools(
454 tools_with_task.clone(),
455 schedule_store.clone(),
456 schedule_manager.clone(),
457 session_store.clone(),
458 storage.clone(),
459 persistence.clone(),
460 spawn_scheduler.clone(),
461 sessions.clone(),
462 agent_runners.clone(),
463 session_event_senders.clone(),
464 subagent_model_resolver,
465 config.clone(),
466 provider_registry.clone(),
467 config_snapshot.subagents.broker.clone(),
468 fabric_deployer.clone(),
469 notification_relay_deps.clone(),
470 );
471
472 child_completion_coordinator
473 .set_root_tools(tools.clone())
474 .await;
475
476 let tool_factory =
477 crate::tools::ToolSurfaceFactory::new(base_tools, tools_with_task, tools);
478
479 let session_repo = bamboo_engine::SessionRepository::new(
480 sessions.clone(),
481 storage.clone(),
482 persistence.clone(),
483 );
484
485 let child_adapter = Arc::new(crate::tools::ChildSessionAdapter {
492 session_store: session_store.clone(),
493 storage: storage.clone(),
494 persistence: persistence.clone(),
495 scheduler: spawn_scheduler.clone(),
496 sessions_cache: sessions.clone(),
497 agent_runners: agent_runners.clone(),
498 session_event_senders: session_event_senders.clone(),
499 subagent_model_resolver: None,
500 config: config.clone(),
501 parent_wait_slots: Arc::new(dashmap::DashMap::new()),
502 notification_relay: Some(notification_relay_deps.clone()),
503 });
504 let guardian_spawner: Arc<dyn bamboo_engine::GuardianSpawner> = child_adapter.clone();
505 child_completion_coordinator
508 .set_guardian_spawner(guardian_spawner.clone())
509 .await;
510
511 let bash_resume_hook: Arc<dyn bamboo_engine::BashResumeHook> =
515 child_completion_coordinator.clone();
516
517 reconcile_fabric_on_boot(&config, &bamboo_home_dir).await;
522
523 Ok(Self {
524 app_data_dir: bamboo_home_dir,
525 config,
526 config_io_lock,
527 fabric_deployer,
528 embedded_broker,
529 health_monitor,
530 provider: provider_lock,
531 provider_handle,
532 sessions,
533 storage,
534 session_store,
535 session_repo,
536 persistence,
537 spawn_scheduler,
538 child_completion_coordinator,
539 guardian_spawner,
540 bash_resume_hook,
541 schedule_store,
542 schedule_manager,
543 tool_factory,
544 permission_checker,
545 notification_service,
546 session_watchers,
547 cancel_tokens: Arc::new(RwLock::new(HashMap::new())),
548 mcp_proxy_shutdown,
549 skill_manager,
550 mcp_manager,
551 metrics_service,
552 agent_runners,
553 session_event_senders,
554 account_sink,
555 process_registry,
556 metrics_bus: None, agent,
558 provider_registry,
559 provider_router,
560 model_catalog,
561 title_gen_in_flight: Arc::new(dashmap::DashSet::new()),
562 pairing_codes: Arc::new(dashmap::DashMap::new()),
563 pairing_code_guard: Arc::new(crate::handlers::settings::PairingCodeGuard::default()),
564 root_password_guard: Arc::new(crate::handlers::settings::RootPasswordGuard::default()),
565 })
567 }
568}
569
570pub struct EmbeddedBroker {
573 task: tokio::task::JoinHandle<()>,
574 gc_task: tokio::task::JoinHandle<()>,
575}
576
577impl Drop for EmbeddedBroker {
578 fn drop(&mut self) {
579 self.task.abort();
580 self.gc_task.abort();
581 }
582}
583
584pub struct HealthMonitor(tokio::task::JoinHandle<()>);
588
589impl Drop for HealthMonitor {
590 fn drop(&mut self) {
591 self.0.abort();
592 }
593}
594
595async fn maybe_embed_broker(
607 config: &mut bamboo_llm::Config,
608 data_dir: &std::path::Path,
609) -> Option<EmbeddedBroker> {
610 if let Some(external) = load_external_broker(data_dir) {
615 let endpoint = external.endpoint.trim().to_string();
616 if broker_endpoint_reachable(&endpoint).await {
617 tracing::info!(%endpoint, "using external broker from broker.json");
618 config.subagents.broker = Some(external);
619 return None;
620 }
621 tracing::warn!(
622 %endpoint,
623 "broker.json endpoint is unreachable — embedding a fresh in-process broker instead"
624 );
625 }
626
627 let listener = match tokio::net::TcpListener::bind("127.0.0.1:0").await {
628 Ok(l) => l,
629 Err(e) => {
630 tracing::warn!("embedded broker: bind failed, sub-agent dispatch disabled: {e}");
631 return None;
632 }
633 };
634 let port = match listener.local_addr() {
635 Ok(a) => a.port(),
636 Err(e) => {
637 tracing::warn!("embedded broker: local_addr failed: {e}");
638 return None;
639 }
640 };
641 let token = uuid::Uuid::new_v4().simple().to_string();
642 let root = data_dir.join("broker");
643 let core = Arc::new(bamboo_broker::BrokerCore::new(root));
644 let gc_task = core
647 .clone()
648 .spawn_mailbox_gc(std::time::Duration::from_secs(300));
649 let server = Arc::new(bamboo_broker::BrokerServer::new(core, token.clone()));
650
651 let task = tokio::spawn(async move {
652 if let Err(e) = server.serve(listener).await {
653 tracing::error!("embedded broker serve loop ended: {e}");
654 }
655 });
656
657 config.subagents.broker = Some(bamboo_config::BrokerClientConfig {
660 endpoint: format!("ws://127.0.0.1:{port}"),
661 token,
662 token_encrypted: None,
663 });
664 tracing::info!(port, "embedded mailbox bus (broker) started in-process");
665 Some(EmbeddedBroker { task, gc_task })
666}
667
668fn load_external_broker(data_dir: &std::path::Path) -> Option<bamboo_config::BrokerClientConfig> {
677 let path = data_dir.join("broker.json");
678 let bytes = std::fs::read(&path).ok()?;
679 match serde_json::from_slice::<bamboo_config::BrokerClientConfig>(&bytes) {
680 Ok(cfg) if !cfg.endpoint.trim().is_empty() => Some(cfg),
681 Ok(_) => {
682 tracing::warn!(?path, "broker.json has an empty endpoint — ignoring");
683 None
684 }
685 Err(e) => {
686 tracing::warn!(?path, "broker.json present but unparseable: {e}");
687 None
688 }
689 }
690}
691
692async fn broker_endpoint_reachable(endpoint: &str) -> bool {
697 let host_port = endpoint
698 .trim()
699 .trim_start_matches("wss://")
700 .trim_start_matches("ws://")
701 .split('/')
702 .next()
703 .unwrap_or("");
704 if host_port.is_empty() {
705 return false;
706 }
707 matches!(
708 tokio::time::timeout(
709 std::time::Duration::from_millis(500),
710 tokio::net::TcpStream::connect(host_port),
711 )
712 .await,
713 Ok(Ok(_))
714 )
715}
716
717async fn reconcile_fabric_on_boot(
722 config: &Arc<RwLock<bamboo_llm::Config>>,
723 data_dir: &std::path::Path,
724) {
725 use bamboo_config::cluster_fabric::NodeStatus;
726
727 let snapshot = {
728 let mut cfg = config.write().await;
729 let mut changed = 0usize;
730 for node in &mut cfg.cluster_fabric.nodes {
731 if let Some(state) = node.state.as_mut() {
732 if matches!(state.status, NodeStatus::Running | NodeStatus::Deploying) {
733 state.status = NodeStatus::Unreachable;
734 state.last_error =
735 Some("orchestrator restarted; worker no longer tracked".to_string());
736 changed += 1;
737 }
738 }
739 }
740 if changed == 0 {
741 return;
742 }
743 tracing::info!(
744 reconciled = changed,
745 "cluster-fabric: marked stale Running nodes Unreachable on boot"
746 );
747 cfg.clone()
748 };
749
750 if let Err(e) = snapshot.save_to_dir(data_dir.to_path_buf()) {
751 tracing::warn!("cluster-fabric boot reconcile: failed to persist: {e}");
752 }
753}
754
755#[cfg(test)]
756mod broker_embed_tests {
757 use super::{broker_endpoint_reachable, load_external_broker};
758
759 #[test]
760 fn load_external_broker_reads_broker_json_not_config() {
761 let dir = tempfile::tempdir().unwrap();
762 assert!(load_external_broker(dir.path()).is_none());
764
765 std::fs::write(
767 dir.path().join("broker.json"),
768 r#"{ "endpoint": "wss://broker.example:9600", "token": "t" }"#,
769 )
770 .unwrap();
771 let got = load_external_broker(dir.path()).expect("parsed");
772 assert_eq!(got.endpoint, "wss://broker.example:9600");
773 assert_eq!(got.token, "t");
774
775 std::fs::write(dir.path().join("broker.json"), r#"{ "endpoint": " " }"#).unwrap();
777 assert!(load_external_broker(dir.path()).is_none());
778
779 std::fs::write(dir.path().join("broker.json"), "not json").unwrap();
781 assert!(load_external_broker(dir.path()).is_none());
782 }
783
784 #[tokio::test]
785 async fn reachability_probe_distinguishes_live_from_dead() {
786 let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
789 let dead = l.local_addr().unwrap();
790 drop(l);
791 assert!(!broker_endpoint_reachable(&format!("ws://{dead}")).await);
792
793 assert!(!broker_endpoint_reachable("").await);
795 assert!(!broker_endpoint_reachable("ws://").await);
796
797 let live = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
799 let addr = live.local_addr().unwrap();
800 assert!(broker_endpoint_reachable(&format!("ws://{addr}/stream")).await);
801 }
802}