Skip to main content

bamboo_server/external_agents/
config.rs

1use serde::Deserialize;
2use std::collections::HashMap;
3
4use bamboo_infrastructure::Config;
5
6#[derive(Debug, Clone, Deserialize)]
7pub struct ExternalAgentProfile {
8    pub agent_id: String,
9    pub protocol: ExternalAgentProtocol,
10    #[serde(default, skip_serializing_if = "Option::is_none")]
11    pub agent_card_url: Option<String>,
12    #[serde(default, skip_serializing_if = "Option::is_none")]
13    pub rpc_url_override: Option<String>,
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub auth_ref: Option<String>,
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub tenant: Option<String>,
18    pub permission_profile: String,
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub skill: Option<String>,
21    #[serde(default)]
22    pub allow_non_streaming_fallback: bool,
23}
24
25#[derive(Debug, Clone, Deserialize)]
26pub enum ExternalAgentProtocol {
27    #[serde(rename = "a2a_jsonrpc")]
28    A2aJsonRpc,
29}
30
31#[derive(Debug, Clone, Deserialize)]
32pub struct SubagentRouting {
33    pub runtime: String, // "external" or "bamboo"
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub agent_id: Option<String>,
36}
37
38/// Parse external agent profiles from `Config.extra["externalAgents"]`.
39pub fn parse_external_agents(config: &Config) -> HashMap<String, ExternalAgentProfile> {
40    let Some(value) = config.extra.get("externalAgents") else {
41        return HashMap::new();
42    };
43
44    match serde_json::from_value(value.clone()) {
45        Ok(agents) => agents,
46        Err(error) => {
47            tracing::error!(
48                "Invalid externalAgents config; external agent routing disabled: {}",
49                error
50            );
51            HashMap::new()
52        }
53    }
54}
55
56/// Parse subagent routing table from `Config.extra["subagentRouting"]`.
57pub fn parse_subagent_routing(config: &Config) -> HashMap<String, SubagentRouting> {
58    let Some(value) = config.extra.get("subagentRouting") else {
59        return HashMap::new();
60    };
61
62    match serde_json::from_value(value.clone()) {
63        Ok(routing) => routing,
64        Err(error) => {
65            tracing::error!(
66                "Invalid subagentRouting config; external subagent routing disabled: {}",
67                error
68            );
69            HashMap::new()
70        }
71    }
72}
73
74/// Resolve runtime metadata for a subagent_type based on config routing.
75pub fn resolve_runtime_metadata(config: &Config, subagent_type: &str) -> HashMap<String, String> {
76    let routing = parse_subagent_routing(config);
77    let agents = parse_external_agents(config);
78
79    let mut metadata = HashMap::new();
80
81    let Some(route) = routing.get(subagent_type) else {
82        return metadata;
83    };
84
85    if route.runtime == "external" {
86        metadata.insert("runtime.kind".to_string(), "external".to_string());
87
88        if let Some(agent_id) = &route.agent_id {
89            metadata.insert("external.agent_id".to_string(), agent_id.clone());
90
91            if let Some(profile) = agents.get(agent_id) {
92                metadata.insert(
93                    "external.protocol".to_string(),
94                    match profile.protocol {
95                        ExternalAgentProtocol::A2aJsonRpc => "a2a_jsonrpc".to_string(),
96                    },
97                );
98                metadata.insert(
99                    "external.permission_profile".to_string(),
100                    profile.permission_profile.clone(),
101                );
102                if let Some(url) = &profile.agent_card_url {
103                    metadata.insert("external.agent_card_url".to_string(), url.clone());
104                }
105            }
106        }
107    }
108
109    metadata
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn parse_external_agents_from_config_extra() {
118        let mut config = Config::default();
119        config.extra.insert(
120            "externalAgents".to_string(),
121            serde_json::json!({
122                "remote_impl": {
123                    "agent_id": "remote_impl",
124                    "protocol": "a2a_jsonrpc",
125                    "agent_card_url": "https://example.com/agent-card.json",
126                    "auth_ref": "REMOTE_IMPL_TOKEN",
127                    "permission_profile": "remote_limited"
128                }
129            }),
130        );
131
132        let agents = parse_external_agents(&config);
133        assert_eq!(agents.len(), 1);
134        let profile = agents.get("remote_impl").unwrap();
135        assert_eq!(profile.agent_id, "remote_impl");
136        assert!(matches!(
137            profile.protocol,
138            ExternalAgentProtocol::A2aJsonRpc
139        ));
140        assert_eq!(
141            profile.agent_card_url,
142            Some("https://example.com/agent-card.json".to_string())
143        );
144        assert_eq!(profile.auth_ref, Some("REMOTE_IMPL_TOKEN".to_string()));
145    }
146
147    #[test]
148    fn parse_subagent_routing_from_config_extra() {
149        let mut config = Config::default();
150        config.extra.insert(
151            "subagentRouting".to_string(),
152            serde_json::json!({
153                "impl": { "runtime": "external", "agent_id": "remote_impl" },
154                "plan": { "runtime": "bamboo" }
155            }),
156        );
157
158        let routing = parse_subagent_routing(&config);
159        assert_eq!(routing.len(), 2);
160        assert_eq!(routing.get("impl").unwrap().runtime, "external");
161        assert_eq!(
162            routing.get("impl").unwrap().agent_id,
163            Some("remote_impl".to_string())
164        );
165        assert_eq!(routing.get("plan").unwrap().runtime, "bamboo");
166    }
167
168    #[test]
169    fn resolve_runtime_metadata_routes_impl_to_external() {
170        let mut config = Config::default();
171        config.extra.insert(
172            "externalAgents".to_string(),
173            serde_json::json!({
174                "remote_impl": {
175                    "agent_id": "remote_impl",
176                    "protocol": "a2a_jsonrpc",
177                    "permission_profile": "remote_limited"
178                }
179            }),
180        );
181        config.extra.insert(
182            "subagentRouting".to_string(),
183            serde_json::json!({
184                "impl": { "runtime": "external", "agent_id": "remote_impl" }
185            }),
186        );
187
188        let metadata = resolve_runtime_metadata(&config, "impl");
189        assert_eq!(metadata.get("runtime.kind"), Some(&"external".to_string()));
190        assert_eq!(
191            metadata.get("external.protocol"),
192            Some(&"a2a_jsonrpc".to_string())
193        );
194        assert_eq!(
195            metadata.get("external.agent_id"),
196            Some(&"remote_impl".to_string())
197        );
198    }
199
200    #[test]
201    fn resolve_runtime_metadata_returns_empty_for_unknown_type() {
202        let config = Config::default();
203        let metadata = resolve_runtime_metadata(&config, "unknown");
204        assert!(metadata.is_empty());
205    }
206}