bamboo_server/external_agents/
config.rs1use 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, #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub agent_id: Option<String>,
36}
37
38pub 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
56pub 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
74pub 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}