use std::sync::Arc;
use bamboo_subagent::provision::{ChildIdentity, ExecutorSpec, ModelRefSpec, ProvisionSpec};
use bamboo_subagent::{AgentRef, EchoExecutor};
use crate::subagent_worker::BambooRuntimeExecutor;
pub struct BrokerAgentArgs {
pub broker: String,
pub token: String,
pub id: String,
pub role: Option<String>,
pub model: Option<String>,
pub workspace: Option<String>,
pub echo: bool,
pub mcp_proxy: Option<String>,
}
pub async fn run(args: BrokerAgentArgs) -> Result<(), String> {
let me = AgentRef {
session_id: args.id.clone(),
role: args.role.clone(),
};
tracing::info!(id = %args.id, broker = %args.broker, echo = args.echo, "broker-agent connecting");
if args.echo {
return bamboo_broker::serve_executor(
&args.broker,
me,
&args.token,
Arc::new(EchoExecutor),
)
.await
.map_err(|e| format!("broker-agent (echo) failed: {e}"));
}
let spec = build_spec(&args)?;
let executor = BambooRuntimeExecutor::build(&spec).await?;
bamboo_broker::serve_executor(&args.broker, me, &args.token, Arc::new(executor))
.await
.map_err(|e| format!("broker-agent failed: {e}"))
}
fn build_spec(args: &BrokerAgentArgs) -> Result<ProvisionSpec, String> {
let config = bamboo_config::Config::new();
let credentials =
bamboo_engine::external_agents::runtime::extract_provider_credentials(&config);
if credentials.is_empty() {
return Err(
"no provider credentials in local config (configure a provider, or use --echo)".into(),
);
}
let mut spec = ProvisionSpec::new(
ChildIdentity {
child_id: args.id.clone(),
parent_id: None,
project_key: None,
role: args
.role
.clone()
.unwrap_or_else(|| "general-purpose".into()),
depth: 0,
},
ExecutorSpec::BambooRuntime,
std::env::temp_dir()
.join("bamboo-broker-agents")
.join(&args.id)
.to_string_lossy()
.into_owned(),
);
spec.model = args.model.as_deref().and_then(parse_model).or_else(|| {
config.defaults.as_ref().and_then(|defaults| {
defaults
.sub_agent
.as_ref()
.or(Some(&defaults.chat))
.map(|r| ModelRefSpec {
provider: r.provider.clone(),
model: r.model.clone(),
})
})
});
spec.workspace = args.workspace.clone();
spec.secrets.provider_credentials = credentials;
if let Some(orchestrator) = &args.mcp_proxy {
spec.capabilities.mcp_proxy = Some(bamboo_subagent::McpProxyConfig {
orchestrator: orchestrator.clone(),
endpoint: args.broker.clone(),
token: args.token.clone(),
});
} else {
let portable = portable_mcp(&config.mcp);
if !portable.servers.is_empty() {
spec.capabilities.mcp = serde_json::to_value(&portable).ok();
}
}
let skills_dir = bamboo_config::paths::resolve_bamboo_dir().join("skills");
if skills_dir.is_dir() {
spec.capabilities.skills_dir = Some(skills_dir.to_string_lossy().into_owned());
}
Ok(spec)
}
fn portable_mcp(
mcp: &bamboo_domain::mcp_config::McpConfig,
) -> bamboo_domain::mcp_config::McpConfig {
use bamboo_domain::mcp_config::TransportConfig;
let servers = mcp
.servers
.iter()
.filter(|s| s.enabled && !matches!(s.transport, TransportConfig::Stdio(_)))
.cloned()
.collect();
bamboo_domain::mcp_config::McpConfig {
version: mcp.version,
servers,
}
}
fn parse_model(s: &str) -> Option<ModelRefSpec> {
let s = s.trim();
if s.is_empty() {
return None;
}
Some(match s.split_once(':') {
Some((p, m)) => ModelRefSpec {
provider: p.trim().to_string(),
model: m.trim().to_string(),
},
None => ModelRefSpec {
provider: String::new(),
model: s.to_string(),
},
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_model_handles_provider_pair_and_bare() {
assert_eq!(
parse_model("anthropic:claude-sonnet-4-6"),
Some(ModelRefSpec {
provider: "anthropic".into(),
model: "claude-sonnet-4-6".into()
})
);
assert_eq!(
parse_model("gpt-5"),
Some(ModelRefSpec {
provider: String::new(),
model: "gpt-5".into()
})
);
assert_eq!(parse_model(" "), None);
}
#[test]
fn portable_mcp_keeps_url_servers_drops_stdio_and_disabled() {
let mcp: bamboo_domain::mcp_config::McpConfig = serde_json::from_value(serde_json::json!({
"version": 1,
"servers": [
{ "id": "web", "enabled": true, "transport": { "type": "sse", "url": "https://w/sse" } },
{ "id": "nova", "enabled": true, "transport": { "type": "stdio", "command": "nova" } },
{ "id": "off", "enabled": false, "transport": { "type": "sse", "url": "https://o/sse" } },
]
}))
.expect("mcp config deserializes");
let portable = portable_mcp(&mcp);
let ids: Vec<_> = portable.servers.iter().map(|s| s.id.clone()).collect();
assert_eq!(ids, vec!["web"]); }
}