Skip to main content

a2a_client/
factory.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// Copyright A2A Contributors (https://github.com/a2aproject)
3// SPDX-License-Identifier: Apache-2.0
4use a2a::*;
5use std::collections::HashMap;
6use std::sync::Arc;
7
8use crate::client::A2AClient;
9use crate::jsonrpc::JsonRpcTransportFactory;
10use crate::middleware::CallInterceptor;
11use crate::rest::RestTransportFactory;
12use crate::transport::TransportFactory;
13
14/// Key for looking up transport factories.
15#[derive(Debug, Clone, Hash, PartialEq, Eq)]
16struct TransportKey {
17    protocol: String,
18    major_version: u64,
19}
20
21impl TransportKey {
22    fn from_interface(iface: &AgentInterface) -> Self {
23        let major = iface
24            .protocol_version
25            .split('.')
26            .next()
27            .and_then(|s| s.parse::<u64>().ok())
28            .unwrap_or(1);
29        TransportKey {
30            protocol: iface.protocol_binding.clone(),
31            major_version: major,
32        }
33    }
34
35    fn from_protocol(protocol: &str, version: &str) -> Self {
36        let major = version
37            .split('.')
38            .next()
39            .and_then(|s| s.parse::<u64>().ok())
40            .unwrap_or(1);
41        TransportKey {
42            protocol: protocol.to_string(),
43            major_version: major,
44        }
45    }
46}
47
48/// Factory for creating [`A2AClient`] instances with automatic protocol negotiation.
49///
50/// Maintains a registry of [`TransportFactory`] implementations indexed by protocol
51/// and major version. When creating a client from an [`AgentCard`], it selects the
52/// best matching transport based on the agent's declared interfaces and the client's
53/// preferred bindings.
54pub struct A2AClientFactory {
55    factories: HashMap<TransportKey, Arc<dyn TransportFactory>>,
56    preferred_bindings: Vec<String>,
57    interceptors: Vec<Arc<dyn CallInterceptor>>,
58}
59
60impl A2AClientFactory {
61    /// Create a builder for configuring the factory.
62    pub fn builder() -> A2AClientFactoryBuilder {
63        A2AClientFactoryBuilder::new()
64    }
65
66    /// Create a client by negotiating the best transport with the agent card.
67    ///
68    /// Selection algorithm (mirrors Go SDK):
69    /// 1. For each interface in the agent card's `supported_interfaces`
70    /// 2. Look up matching factory by (protocol, major_version)
71    /// 3. Rank candidates: client preference order, then newest version
72    /// 4. Try connecting in rank order; first success wins
73    pub async fn create_from_card(
74        &self,
75        card: &AgentCard,
76    ) -> Result<A2AClient<Box<dyn crate::Transport>>, A2AError> {
77        self.create_from_card_selecting(card)
78            .await
79            .map(|(client, _)| client)
80    }
81
82    /// Same negotiation as [`Self::create_from_card`], but also returns the
83    /// [`AgentInterface`] that was selected — needed by a caller that must
84    /// honor that interface's own declared routing `tenant` (A2A §8.3.2) on
85    /// every subsequent request, which the interface list alone doesn't
86    /// otherwise surface once a client has been built.
87    pub async fn create_from_card_with_interface(
88        &self,
89        card: &AgentCard,
90    ) -> Result<(A2AClient<Box<dyn crate::Transport>>, AgentInterface), A2AError> {
91        self.create_from_card_selecting(card).await
92    }
93
94    async fn create_from_card_selecting(
95        &self,
96        card: &AgentCard,
97    ) -> Result<(A2AClient<Box<dyn crate::Transport>>, AgentInterface), A2AError> {
98        let mut candidates: Vec<(usize, &AgentInterface, &Arc<dyn TransportFactory>)> = Vec::new();
99
100        for iface in &card.supported_interfaces {
101            let key = TransportKey::from_interface(iface);
102            if let Some(factory) = self.factories.get(&key) {
103                let priority = self
104                    .preferred_bindings
105                    .iter()
106                    .position(|b| b == &iface.protocol_binding)
107                    .unwrap_or(usize::MAX);
108                candidates.push((priority, iface, factory));
109            }
110        }
111
112        if candidates.is_empty() {
113            return Err(A2AError::unsupported_operation(
114                "no compatible transport found for agent card interfaces",
115            ));
116        }
117
118        // Sort by preference (lower is better)
119        candidates.sort_by_key(|(prio, _, _)| *prio);
120
121        let mut last_err = None;
122        for (_prio, iface, factory) in &candidates {
123            match factory.create(card, iface).await {
124                Ok(transport) => {
125                    let mut client =
126                        A2AClient::new(transport).with_interceptors(self.interceptors.clone());
127                    // A2A §8.3.2 rule 4: echo the tenant the selected
128                    // interface declares on every request. An empty string
129                    // is proto3's default for an unset field, so it means
130                    // "not declared" rather than a tenant named "".
131                    if let Some(tenant) = iface.tenant.as_deref().filter(|t| !t.is_empty()) {
132                        client = client.with_tenant(tenant);
133                    }
134                    return Ok((client, (*iface).clone()));
135                }
136                Err(e) => {
137                    tracing::debug!(
138                        protocol = %iface.protocol_binding,
139                        url = %iface.url,
140                        error = %e,
141                        "transport creation failed, trying next"
142                    );
143                    last_err = Some(e);
144                }
145            }
146        }
147
148        Err(last_err.unwrap_or_else(|| A2AError::internal("failed to create transport")))
149    }
150}
151
152/// Builder for [`A2AClientFactory`].
153pub struct A2AClientFactoryBuilder {
154    factories: HashMap<TransportKey, Arc<dyn TransportFactory>>,
155    preferred_bindings: Vec<String>,
156    interceptors: Vec<Arc<dyn CallInterceptor>>,
157    include_defaults: bool,
158}
159
160impl A2AClientFactoryBuilder {
161    fn new() -> Self {
162        A2AClientFactoryBuilder {
163            factories: HashMap::new(),
164            preferred_bindings: vec![
165                TRANSPORT_PROTOCOL_JSONRPC.to_string(),
166                TRANSPORT_PROTOCOL_HTTP_JSON.to_string(),
167            ],
168            interceptors: Vec::new(),
169            include_defaults: true,
170        }
171    }
172
173    /// Register a transport factory for a protocol binding.
174    pub fn register(mut self, factory: Arc<dyn TransportFactory>) -> Self {
175        let key = TransportKey::from_protocol(factory.protocol(), VERSION);
176        self.factories.insert(key, factory);
177        self
178    }
179
180    /// Set preferred binding order. First is most preferred.
181    pub fn preferred_bindings(mut self, bindings: Vec<String>) -> Self {
182        self.preferred_bindings = bindings;
183        self
184    }
185
186    /// Add a call interceptor.
187    pub fn with_interceptor(mut self, interceptor: Arc<dyn CallInterceptor>) -> Self {
188        self.interceptors.push(interceptor);
189        self
190    }
191
192    /// Disable default JSON-RPC and REST transport factories.
193    pub fn no_defaults(mut self) -> Self {
194        self.include_defaults = false;
195        self
196    }
197
198    /// Build the factory.
199    pub fn build(mut self) -> A2AClientFactory {
200        if self.include_defaults {
201            let jsonrpc_key = TransportKey::from_protocol(TRANSPORT_PROTOCOL_JSONRPC, VERSION);
202            self.factories
203                .entry(jsonrpc_key)
204                .or_insert_with(|| Arc::new(JsonRpcTransportFactory::new(None)));
205
206            let rest_key = TransportKey::from_protocol(TRANSPORT_PROTOCOL_HTTP_JSON, VERSION);
207            self.factories
208                .entry(rest_key)
209                .or_insert_with(|| Arc::new(RestTransportFactory::new(None)));
210        }
211
212        A2AClientFactory {
213            factories: self.factories,
214            preferred_bindings: self.preferred_bindings,
215            interceptors: self.interceptors,
216        }
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_transport_key_from_interface() {
226        let iface = AgentInterface::new("http://localhost", "jsonrpc");
227        let key = TransportKey::from_interface(&iface);
228        assert_eq!(key.protocol, "jsonrpc");
229    }
230
231    #[test]
232    fn test_transport_key_from_interface_no_version() {
233        let mut iface = AgentInterface::new("http://localhost", "rest");
234        iface.protocol_version = "bad".to_string();
235        let key = TransportKey::from_interface(&iface);
236        assert_eq!(key.major_version, 1); // defaults to 1
237    }
238
239    #[test]
240    fn test_transport_key_from_protocol() {
241        let key = TransportKey::from_protocol("jsonrpc", "2.3.4");
242        assert_eq!(key.protocol, "jsonrpc");
243        assert_eq!(key.major_version, 2);
244    }
245
246    #[test]
247    fn test_builder_defaults() {
248        let factory = A2AClientFactory::builder().build();
249        assert_eq!(factory.factories.len(), 2); // jsonrpc + rest
250        assert_eq!(factory.preferred_bindings.len(), 2);
251    }
252
253    #[test]
254    fn test_builder_no_defaults() {
255        let factory = A2AClientFactory::builder().no_defaults().build();
256        assert!(factory.factories.is_empty());
257    }
258
259    #[test]
260    fn test_builder_preferred_bindings() {
261        let factory = A2AClientFactory::builder()
262            .preferred_bindings(vec!["grpc".to_string()])
263            .build();
264        assert_eq!(factory.preferred_bindings, vec!["grpc"]);
265    }
266
267    #[test]
268    fn test_builder_with_interceptor() {
269        use crate::middleware::LoggingInterceptor;
270        let factory = A2AClientFactory::builder()
271            .with_interceptor(Arc::new(LoggingInterceptor))
272            .build();
273        assert_eq!(factory.interceptors.len(), 1);
274    }
275
276    #[tokio::test]
277    async fn test_create_from_card_no_matching_transport() {
278        let factory = A2AClientFactory::builder().no_defaults().build();
279        let card = AgentCard {
280            name: "test".into(),
281            description: "test agent".into(),
282            version: "1.0".into(),
283            supported_interfaces: vec![AgentInterface::new("http://localhost", "unknown")],
284            capabilities: AgentCapabilities::default(),
285            default_input_modes: vec!["text".into()],
286            default_output_modes: vec!["text".into()],
287            skills: vec![],
288            provider: None,
289            documentation_url: None,
290            icon_url: None,
291            security_schemes: None,
292            security_requirements: None,
293            signatures: None,
294        };
295        let result = factory.create_from_card(&card).await;
296        assert!(result.is_err());
297    }
298
299    #[tokio::test]
300    async fn test_create_from_card_with_interface_returns_selected_interface() {
301        let factory = A2AClientFactory::builder().build();
302        let mut iface = AgentInterface::new("http://localhost/jsonrpc", TRANSPORT_PROTOCOL_JSONRPC);
303        iface.tenant = Some("tenant-42".to_string());
304        let card = AgentCard {
305            name: "test".into(),
306            description: "test agent".into(),
307            version: "1.0".into(),
308            supported_interfaces: vec![iface],
309            capabilities: AgentCapabilities::default(),
310            default_input_modes: vec!["text".into()],
311            default_output_modes: vec!["text".into()],
312            skills: vec![],
313            provider: None,
314            documentation_url: None,
315            icon_url: None,
316            security_schemes: None,
317            security_requirements: None,
318            signatures: None,
319        };
320
321        let (_client, selected) = factory
322            .create_from_card_with_interface(&card)
323            .await
324            .unwrap();
325        assert_eq!(selected.protocol_binding, TRANSPORT_PROTOCOL_JSONRPC);
326        assert_eq!(selected.tenant.as_deref(), Some("tenant-42"));
327    }
328
329    fn card_with_tenant(tenant: Option<&str>) -> AgentCard {
330        let mut iface = AgentInterface::new("http://localhost/jsonrpc", TRANSPORT_PROTOCOL_JSONRPC);
331        iface.tenant = tenant.map(str::to_string);
332        AgentCard {
333            name: "test".into(),
334            description: "test agent".into(),
335            version: "1.0".into(),
336            supported_interfaces: vec![iface],
337            capabilities: AgentCapabilities::default(),
338            default_input_modes: vec!["text".into()],
339            default_output_modes: vec!["text".into()],
340            skills: vec![],
341            provider: None,
342            documentation_url: None,
343            icon_url: None,
344            security_schemes: None,
345            security_requirements: None,
346            signatures: None,
347        }
348    }
349
350    /// A2A §8.3.2 rule 4: the factory carries the declared tenant onto the
351    /// client, so every request echoes it without the caller doing anything.
352    #[tokio::test]
353    async fn test_create_from_card_applies_the_declared_tenant() {
354        let factory = A2AClientFactory::builder().build();
355        let client = factory
356            .create_from_card(&card_with_tenant(Some("tenant-42")))
357            .await
358            .unwrap();
359        assert_eq!(client.tenant(), Some("tenant-42"));
360    }
361
362    /// An entry that declares no tenant leaves the client with none, so the
363    /// field stays omitted rather than being defaulted.
364    #[tokio::test]
365    async fn test_create_from_card_without_a_declared_tenant() {
366        let factory = A2AClientFactory::builder().build();
367        let client = factory
368            .create_from_card(&card_with_tenant(None))
369            .await
370            .unwrap();
371        assert_eq!(client.tenant(), None);
372    }
373
374    /// The empty string is proto3's default for an unset field, so it means
375    /// "not declared" — not a tenant named "".
376    #[tokio::test]
377    async fn test_create_from_card_treats_an_empty_tenant_as_undeclared() {
378        let factory = A2AClientFactory::builder().build();
379        let client = factory
380            .create_from_card(&card_with_tenant(Some("")))
381            .await
382            .unwrap();
383        assert_eq!(client.tenant(), None);
384    }
385}