use super::init::{
build_provider_handles, build_schedule_manager, build_spawn_scheduler, init_mcp_manager,
init_metrics_service, init_schedule_store, init_skill_manager, init_storage,
load_permission_checker, spawn_runner_cleanup_task,
};
use super::tools::{build_base_tools, build_root_tools};
use super::*;
use crate::tools::OptionalSubagentModelResolver;
use bamboo_agent_core::storage::Storage;
impl AppState {
pub async fn new(bamboo_home_dir: PathBuf) -> Result<Self, AppError> {
bamboo_config::paths::init_bamboo_dir(bamboo_home_dir.clone());
let config = Config::from_data_dir(Some(bamboo_home_dir.clone()));
let provider_registry =
match bamboo_llm::ProviderRegistry::from_config(&config, bamboo_home_dir.clone()).await
{
Ok(registry) => Arc::new(registry),
Err(e) => {
tracing::error!("Failed to create provider registry: {}", e);
Arc::new(
bamboo_llm::ProviderRegistry::from_config(
&Config::default(),
bamboo_home_dir.clone(),
)
.await
.expect("Cannot create even an empty provider registry"),
)
}
};
let provider = provider_registry.get_default().unwrap_or_else(|| {
let default_provider_name = provider_registry.default_provider_name();
let message = if config.has_provider_instances() {
format!(
"Default provider instance '{}' is not available or failed to initialize",
default_provider_name
)
} else {
format!(
"Provider '{}' is not available or failed to initialize",
config.provider
)
};
Arc::new(UnconfiguredProvider { message }) as Arc<dyn LLMProvider>
});
Self::new_with_provider(bamboo_home_dir, config, provider).await
}
pub async fn new_with_provider(
bamboo_home_dir: PathBuf,
config: Config,
provider: Arc<dyn LLMProvider>,
) -> Result<Self, AppError> {
let data_dir = bamboo_home_dir.clone();
let (session_store, storage) = init_storage(&data_dir).await?;
let persistence = Arc::new(LockedSessionStore::new(storage.clone()));
let sessions: bamboo_engine::SessionCache = Arc::new(dashmap::DashMap::new());
let mut config = config;
let embedded_broker = maybe_embed_broker(&mut config, &data_dir).await;
let config = Arc::new(RwLock::new(config));
{
let config_for_workspace = config.clone();
let last_known: Arc<std::sync::Mutex<Option<PathBuf>>> =
Arc::new(std::sync::Mutex::new(None));
bamboo_agent_core::workspace_state::set_default_workspace_provider(Box::new(
move || match config_for_workspace.try_read() {
Ok(cfg) => {
let path = cfg.get_default_work_area_path();
if let Ok(mut cache) = last_known.lock() {
*cache = path.clone();
}
path
}
Err(_) => last_known.lock().ok().and_then(|c| c.clone()),
},
));
}
let permission_checker = load_permission_checker(&bamboo_home_dir).await;
let notification_service = Arc::new(bamboo_notification::NotificationService::new(
bamboo_home_dir.join("notification_preferences.json"),
));
let mcp_manager = init_mcp_manager(config.clone());
let skill_manager = init_skill_manager(&data_dir).await;
let metrics_service = init_metrics_service(&data_dir).await?;
let startup_sessions = {
let entries = session_store.list_index_entries().await;
let mut sessions = Vec::new();
for entry in entries {
if let Some(session) = session_store
.load_session(&entry.id)
.await
.map_err(AppError::StorageError)?
{
sessions.push(session);
}
}
sessions
};
metrics_service
.reconcile_startup_sessions(startup_sessions, &[])
.await
.map_err(|error| {
AppError::InternalError(anyhow::anyhow!(
"Failed to reconcile stale metrics state on startup: {error}"
))
})?;
let agent_runners: Arc<RwLock<HashMap<String, AgentRunner>>> =
Arc::new(RwLock::new(HashMap::new()));
spawn_runner_cleanup_task(agent_runners.clone(), None);
let process_registry = Arc::new(ProcessRegistry::new());
let (provider_lock, provider_handle) = build_provider_handles(provider);
let config_snapshot = config.read().await;
let provider_registry = match bamboo_llm::ProviderRegistry::from_config(
&config_snapshot,
bamboo_home_dir.clone(),
)
.await
{
Ok(registry) => Arc::new(registry),
Err(e) => {
tracing::error!("Failed to create provider registry: {}", e);
Arc::new(
bamboo_llm::ProviderRegistry::from_config(
&Config::default(),
bamboo_home_dir.clone(),
)
.await
.expect("Cannot create even an empty provider registry"),
)
}
};
drop(config_snapshot);
let provider_router = Arc::new(bamboo_llm::ProviderModelRouter::new(
provider_registry.clone(),
));
let model_catalog = Arc::new(bamboo_llm::ModelCatalogService::new(
provider_registry.clone(),
));
let base_tools = build_base_tools(
config.clone(),
permission_checker.clone(),
mcp_manager.clone(),
skill_manager.clone(),
storage.clone(),
persistence.clone(),
sessions.clone(),
bamboo_home_dir.clone(),
);
let session_event_senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>> =
Arc::new(RwLock::new(HashMap::new()));
let account_sink = bamboo_engine::events::AccountEventSink::new(data_dir.join("events"))
.map_err(|e| {
AppError::InternalError(anyhow::anyhow!(
"failed to initialize account change-feed journal: {e}"
))
})?;
let child_tools: Arc<dyn bamboo_agent_core::tools::ToolExecutor> = base_tools.clone();
let agent = Arc::new(
bamboo_engine::Agent::builder()
.storage(storage.clone())
.persistence(persistence.clone())
.attachment_reader(session_store.clone())
.skill_manager(skill_manager.clone())
.metrics_collector(metrics_service.collector())
.config(config.clone())
.provider(provider_handle.clone())
.default_tools(base_tools.clone())
.build()
.expect("agent runtime should be fully configured"),
);
let child_completion_coordinator =
Arc::new(bamboo_engine::ChildCompletionCoordinator::new(
storage.clone(),
persistence.clone(),
sessions.clone(),
agent_runners.clone(),
session_event_senders.clone(),
agent.clone(),
config.clone(),
provider_registry.clone(),
provider_router.clone(),
data_dir.clone(),
Some(account_sink.inbox()),
));
let config_snapshot = config.read().await.clone();
let mcp_proxy_shutdown = tokio_util::sync::CancellationToken::new();
if let Some(broker) = config_snapshot.subagents.broker.clone() {
if !broker.endpoint.trim().is_empty() {
let backend: std::sync::Arc<dyn bamboo_agent_core::tools::ToolExecutor> =
std::sync::Arc::new(bamboo_mcp::executor::McpToolExecutor::new(
mcp_manager.clone(),
mcp_manager.tool_index(),
));
let shutdown = mcp_proxy_shutdown.clone();
tokio::spawn(async move {
let me = bamboo_broker::AgentRef {
session_id: bamboo_broker::ORCHESTRATOR_ID.to_string(),
role: Some("orchestrator".into()),
};
let allowlist =
std::sync::Arc::new(bamboo_broker::RoleToolAllowlist::unrestricted());
bamboo_broker::serve_mcp_proxy_supervised(
&broker.endpoint,
me,
&broker.token,
backend,
allowlist,
shutdown,
)
.await;
});
}
}
let external_runner =
bamboo_engine::external_agents::runtime::build_external_child_runner(&config_snapshot);
let spawn_scheduler = build_spawn_scheduler(
agent.clone(),
child_tools,
sessions.clone(),
agent_runners.clone(),
session_event_senders.clone(),
external_runner,
Some(provider_router.clone()),
Some(child_completion_coordinator.clone()),
Some(data_dir.clone()),
Some(account_sink.inbox()),
);
let tools_with_task = base_tools.clone();
let schedule_store = init_schedule_store(&data_dir).await?;
let schedule_manager = build_schedule_manager(
schedule_store.clone(),
agent.clone(),
tools_with_task.clone(),
sessions.clone(),
agent_runners.clone(),
session_event_senders.clone(),
persistence.clone(),
config.clone(),
provider_registry.clone(),
Some(data_dir.clone()),
Some(account_sink.inbox()),
);
bamboo_engine::auto_dream::spawn_auto_dream_task(
bamboo_engine::auto_dream::AutoDreamContext {
session_store: session_store.clone(),
storage: storage.clone(),
provider: provider_handle.clone(),
config: config.clone(),
provider_registry: provider_registry.clone(),
},
);
bamboo_engine::gardener::spawn_gardener_task(bamboo_engine::auto_dream::AutoDreamContext {
session_store: session_store.clone(),
storage: storage.clone(),
provider: provider_handle.clone(),
config: config.clone(),
provider_registry: provider_registry.clone(),
});
let config_for_resolver = config.clone();
let subagent_model_resolver: OptionalSubagentModelResolver = {
let registry = provider_registry.clone();
Some(Arc::new(
move |subagent_type: String| -> futures::future::BoxFuture<
'static,
Option<bamboo_domain::ProviderModelRef>,
> {
let config_for_resolver = config_for_resolver.clone();
let registry = registry.clone();
Box::pin(async move {
let config_snap = config_for_resolver.read().await.clone();
bamboo_engine::model_config_helper::resolve_subagent_model_ref(
&config_snap,
&config_snap.provider,
®istry,
&subagent_type,
)
})
},
))
};
let config_io_lock = Arc::new(tokio::sync::Mutex::new(()));
let fabric_registry: crate::tools::DeployedRegistry =
Arc::new(tokio::sync::Mutex::new(HashMap::new()));
let fabric_bamboo_bin =
std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("bamboo"));
let fabric_deployer = Arc::new(bamboo_server_tools::FabricDeployer::new(
config.clone(),
config_io_lock.clone(),
bamboo_home_dir.clone(),
fabric_registry,
fabric_bamboo_bin,
));
let health_monitor = fabric_deployer
.clone()
.spawn_health_monitor()
.await
.map(HealthMonitor);
let tools = build_root_tools(
tools_with_task.clone(),
schedule_store.clone(),
schedule_manager.clone(),
session_store.clone(),
storage.clone(),
persistence.clone(),
spawn_scheduler.clone(),
sessions.clone(),
agent_runners.clone(),
session_event_senders.clone(),
subagent_model_resolver,
config.clone(),
provider_registry.clone(),
config_snapshot.subagents.broker.clone(),
fabric_deployer.clone(),
);
child_completion_coordinator
.set_root_tools(tools.clone())
.await;
let tool_factory =
crate::tools::ToolSurfaceFactory::new(base_tools, tools_with_task, tools);
let session_repo = bamboo_engine::SessionRepository::new(
sessions.clone(),
storage.clone(),
persistence.clone(),
);
let child_adapter = Arc::new(crate::tools::ChildSessionAdapter {
session_store: session_store.clone(),
storage: storage.clone(),
persistence: persistence.clone(),
scheduler: spawn_scheduler.clone(),
sessions_cache: sessions.clone(),
agent_runners: agent_runners.clone(),
session_event_senders: session_event_senders.clone(),
subagent_model_resolver: None,
config: config.clone(),
parent_wait_slots: Arc::new(dashmap::DashMap::new()),
});
let guardian_spawner: Arc<dyn bamboo_engine::GuardianSpawner> = child_adapter.clone();
child_completion_coordinator
.set_guardian_spawner(guardian_spawner.clone())
.await;
let bash_resume_hook: Arc<dyn bamboo_engine::BashResumeHook> =
child_completion_coordinator.clone();
reconcile_fabric_on_boot(&config, &bamboo_home_dir).await;
Ok(Self {
app_data_dir: bamboo_home_dir,
config,
config_io_lock,
fabric_deployer,
embedded_broker,
health_monitor,
provider: provider_lock,
provider_handle,
sessions,
storage,
session_store,
session_repo,
persistence,
spawn_scheduler,
child_completion_coordinator,
guardian_spawner,
bash_resume_hook,
schedule_store,
schedule_manager,
tool_factory,
permission_checker,
notification_service,
cancel_tokens: Arc::new(RwLock::new(HashMap::new())),
mcp_proxy_shutdown,
skill_manager,
mcp_manager,
metrics_service,
agent_runners,
session_event_senders,
account_sink,
process_registry,
metrics_bus: None, agent,
provider_registry,
provider_router,
model_catalog,
title_gen_in_flight: Arc::new(dashmap::DashSet::new()),
pairing_codes: Arc::new(dashmap::DashMap::new()),
pairing_code_guard: Arc::new(crate::handlers::settings::PairingCodeGuard::default()),
root_password_guard: Arc::new(crate::handlers::settings::RootPasswordGuard::default()),
})
}
}
pub struct EmbeddedBroker {
task: tokio::task::JoinHandle<()>,
gc_task: tokio::task::JoinHandle<()>,
}
impl Drop for EmbeddedBroker {
fn drop(&mut self) {
self.task.abort();
self.gc_task.abort();
}
}
pub struct HealthMonitor(tokio::task::JoinHandle<()>);
impl Drop for HealthMonitor {
fn drop(&mut self) {
self.0.abort();
}
}
async fn maybe_embed_broker(
config: &mut bamboo_llm::Config,
data_dir: &std::path::Path,
) -> Option<EmbeddedBroker> {
if let Some(external) = load_external_broker(data_dir) {
let endpoint = external.endpoint.trim().to_string();
if broker_endpoint_reachable(&endpoint).await {
tracing::info!(%endpoint, "using external broker from broker.json");
config.subagents.broker = Some(external);
return None;
}
tracing::warn!(
%endpoint,
"broker.json endpoint is unreachable — embedding a fresh in-process broker instead"
);
}
let listener = match tokio::net::TcpListener::bind("127.0.0.1:0").await {
Ok(l) => l,
Err(e) => {
tracing::warn!("embedded broker: bind failed, sub-agent dispatch disabled: {e}");
return None;
}
};
let port = match listener.local_addr() {
Ok(a) => a.port(),
Err(e) => {
tracing::warn!("embedded broker: local_addr failed: {e}");
return None;
}
};
let token = uuid::Uuid::new_v4().simple().to_string();
let root = data_dir.join("broker");
let core = Arc::new(bamboo_broker::BrokerCore::new(root));
let gc_task = core
.clone()
.spawn_mailbox_gc(std::time::Duration::from_secs(300));
let server = Arc::new(bamboo_broker::BrokerServer::new(core, token.clone()));
let task = tokio::spawn(async move {
if let Err(e) = server.serve(listener).await {
tracing::error!("embedded broker serve loop ended: {e}");
}
});
config.subagents.broker = Some(bamboo_config::BrokerClientConfig {
endpoint: format!("ws://127.0.0.1:{port}"),
token,
token_encrypted: None,
});
tracing::info!(port, "embedded mailbox bus (broker) started in-process");
Some(EmbeddedBroker { task, gc_task })
}
fn load_external_broker(data_dir: &std::path::Path) -> Option<bamboo_config::BrokerClientConfig> {
let path = data_dir.join("broker.json");
let bytes = std::fs::read(&path).ok()?;
match serde_json::from_slice::<bamboo_config::BrokerClientConfig>(&bytes) {
Ok(cfg) if !cfg.endpoint.trim().is_empty() => Some(cfg),
Ok(_) => {
tracing::warn!(?path, "broker.json has an empty endpoint — ignoring");
None
}
Err(e) => {
tracing::warn!(?path, "broker.json present but unparseable: {e}");
None
}
}
}
async fn broker_endpoint_reachable(endpoint: &str) -> bool {
let host_port = endpoint
.trim()
.trim_start_matches("wss://")
.trim_start_matches("ws://")
.split('/')
.next()
.unwrap_or("");
if host_port.is_empty() {
return false;
}
matches!(
tokio::time::timeout(
std::time::Duration::from_millis(500),
tokio::net::TcpStream::connect(host_port),
)
.await,
Ok(Ok(_))
)
}
async fn reconcile_fabric_on_boot(
config: &Arc<RwLock<bamboo_llm::Config>>,
data_dir: &std::path::Path,
) {
use bamboo_config::cluster_fabric::NodeStatus;
let snapshot = {
let mut cfg = config.write().await;
let mut changed = 0usize;
for node in &mut cfg.cluster_fabric.nodes {
if let Some(state) = node.state.as_mut() {
if matches!(state.status, NodeStatus::Running | NodeStatus::Deploying) {
state.status = NodeStatus::Unreachable;
state.last_error =
Some("orchestrator restarted; worker no longer tracked".to_string());
changed += 1;
}
}
}
if changed == 0 {
return;
}
tracing::info!(
reconciled = changed,
"cluster-fabric: marked stale Running nodes Unreachable on boot"
);
cfg.clone()
};
if let Err(e) = snapshot.save_to_dir(data_dir.to_path_buf()) {
tracing::warn!("cluster-fabric boot reconcile: failed to persist: {e}");
}
}
#[cfg(test)]
mod broker_embed_tests {
use super::{broker_endpoint_reachable, load_external_broker};
#[test]
fn load_external_broker_reads_broker_json_not_config() {
let dir = tempfile::tempdir().unwrap();
assert!(load_external_broker(dir.path()).is_none());
std::fs::write(
dir.path().join("broker.json"),
r#"{ "endpoint": "wss://broker.example:9600", "token": "t" }"#,
)
.unwrap();
let got = load_external_broker(dir.path()).expect("parsed");
assert_eq!(got.endpoint, "wss://broker.example:9600");
assert_eq!(got.token, "t");
std::fs::write(dir.path().join("broker.json"), r#"{ "endpoint": " " }"#).unwrap();
assert!(load_external_broker(dir.path()).is_none());
std::fs::write(dir.path().join("broker.json"), "not json").unwrap();
assert!(load_external_broker(dir.path()).is_none());
}
#[tokio::test]
async fn reachability_probe_distinguishes_live_from_dead() {
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let dead = l.local_addr().unwrap();
drop(l);
assert!(!broker_endpoint_reachable(&format!("ws://{dead}")).await);
assert!(!broker_endpoint_reachable("").await);
assert!(!broker_endpoint_reachable("ws://").await);
let live = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = live.local_addr().unwrap();
assert!(broker_endpoint_reachable(&format!("ws://{addr}/stream")).await);
}
}