Skip to main content

a2a_client/
lib.rs

1//! # a2a-client
2//!
3//! Reusable Rust library for building web-based frontends for A2A (Agent-to-Agent) Protocol agents.
4//!
5//! ## Overview
6//!
7//! This library provides components and utilities for creating web applications that interact
8//! with A2A protocol agents. It wraps the lower-level [`a2a-rs`](https://docs.rs/a2a-rs) clients
9//! with a web-friendly API and includes ready-to-use components for common use cases.
10//!
11//! ## Features
12//!
13//! - **Unified Client API** - Single interface for both HTTP and WebSocket transports
14//! - **SSE Streaming** - Server-Sent Events with automatic fallback to HTTP polling
15//! - **View Models** - Ready-to-use view models for tasks and messages
16//! - **Auto-reconnection** - Resilient WebSocket connections with retry logic
17//! - **Type-safe** - Leverages Rust's type system for protocol correctness
18//!
19//! ## Quick Start
20//!
21//! ### Basic HTTP Client
22//!
23//! ```rust,no_run
24//! use a2a_client::WebA2AClient;
25//! use a2a_rs::domain::Message;
26//! use a2a_rs::services::AsyncA2AClient;
27//!
28//! # #[tokio::main]
29//! # async fn main() -> anyhow::Result<()> {
30//! // Create a client connected to your A2A agent
31//! let client = WebA2AClient::new_http("http://localhost:8080".to_string());
32//!
33//! // Send a message
34//! let message = Message::user_text("Hello, agent!".to_string(), "msg-1".to_string());
35//!
36//! let task = client.http.send_task_message("task-1", &message, None, None).await?;
37//! println!("Task ID: {}", task.id);
38//! # Ok(())
39//! # }
40//! ```
41//!
42//! ### With SSE Streaming via ConnectRPC
43//!
44//! ```rust
45//! use a2a_client::WebA2AClient;
46//!
47//! // Create client with HTTP (which handles ConnectRPC streaming automatically)
48//! let client = WebA2AClient::new_http("http://localhost:8080".to_string());
49//!
50//! println!("HTTP client configured for streaming!");
51//! ```
52//!
53//! ### SSE Streaming with Axum (requires `axum-components` feature)
54//!
55//! ```rust,ignore
56//! # #[cfg(feature = "axum-components")]
57//! # {
58//! use a2a_client::{WebA2AClient, components::create_sse_stream};
59//! use axum::{Router, routing::get, extract::{State, Path}};
60//! use std::sync::Arc;
61//!
62//! # #[tokio::main]
63//! # async fn main() -> anyhow::Result<()> {
64//! let client = Arc::new(WebA2AClient::new_http("http://localhost:8080".to_string()));
65//!
66//! let app = Router::new()
67//!     .route("/stream/:task_id", get(stream_handler))
68//!     .with_state(client);
69//!
70//! // Start your Axum server...
71//! # Ok(())
72//! # }
73//!
74//! async fn stream_handler(
75//!     State(client): State<Arc<WebA2AClient>>,
76//!     Path(task_id): Path<String>,
77//! ) -> axum::response::sse::Sse<impl futures::Stream<Item = Result<axum::response::sse::Event, std::convert::Infallible>>> {
78//!     create_sse_stream(client, task_id)
79//! }
80//! # }
81//! ```
82//!
83//! ## Components
84//!
85//! - [`WebA2AClient`] - Main client wrapper for HTTP and WebSocket transports
86//! - [`components::TaskView`] - View model for displaying tasks in lists
87//! - [`components::MessageView`] - View model for displaying individual messages
88//! - [`components::create_sse_stream`] - SSE stream creation with auto-fallback (requires `axum-components`)
89//! - [`utils::formatters`] - Formatting utilities for A2A types
90//!
91//! ## Feature Flags
92//!
93//! - `axum-components` (default) - Enables Axum-specific SSE streaming components
94//!
95//! ## Integration
96//!
97//! This library integrates with:
98//! - [`a2a-rs`](https://docs.rs/a2a-rs) - Core A2A protocol implementation
99//! - [`a2a-agents`](https://docs.rs/a2a-agents) - Declarative agent framework
100//! - Any agent implementing the A2A Protocol v1.0.0
101//!
102//! ## Examples
103//!
104//! See the `examples/` directory for complete working examples of different use cases.
105
106pub mod components;
107pub mod error;
108pub mod utils;
109
110// Re-export commonly used types
111pub use error::{ClientError, Result};
112
113use a2a_rs::HttpClient;
114
115/// Web-friendly A2A client that wraps both HTTP and WebSocket clients.
116///
117/// This is the main entry point for interacting with A2A agents from web applications.
118/// It provides a unified interface for both HTTP and WebSocket transports, with automatic
119/// fallback and retry logic.
120///
121/// # Examples
122///
123/// ## HTTP-only client
124///
125/// ```rust
126/// use a2a_client::WebA2AClient;
127///
128/// let client = WebA2AClient::new_http("http://localhost:8080".to_string());
129/// ```
130///
131/// ## Client configured for streaming
132///
133/// ```rust
134/// use a2a_client::WebA2AClient;
135///
136/// let client = WebA2AClient::new_http("http://localhost:8080".to_string());
137/// ```
138///
139/// ## Auto-detecting transports
140///
141/// ```rust,no_run
142/// use a2a_client::WebA2AClient;
143///
144/// # #[tokio::main]
145/// # async fn main() -> anyhow::Result<()> {
146/// let client = WebA2AClient::auto_connect("http://localhost:8080").await?;
147/// # Ok(())
148/// # }
149/// ```
150pub struct WebA2AClient {
151    /// HTTP client for A2A requests and streaming
152    pub http: HttpClient,
153}
154
155impl WebA2AClient {
156    /// Create a builder for configuring the client.
157    ///
158    /// # Examples
159    ///
160    /// ```rust
161    /// use a2a_client::WebA2AClient;
162    ///
163    /// let client = WebA2AClient::builder()
164    ///     .http_url("http://localhost:8080")
165    ///     .build();
166    /// ```
167    pub fn builder() -> WebA2AClientBuilder {
168        WebA2AClientBuilder::default()
169    }
170
171    /// Create a new client with HTTP transport only.
172    ///
173    /// # Arguments
174    ///
175    /// * `base_url` - Base URL of the A2A agent (e.g., `http://localhost:8080`)
176    ///
177    /// # Examples
178    ///
179    /// ```rust
180    /// use a2a_client::WebA2AClient;
181    ///
182    /// let client = WebA2AClient::new_http("http://localhost:8080".to_string());
183    /// ```
184    pub fn new_http(base_url: String) -> Self {
185        Self {
186            http: HttpClient::new(base_url),
187        }
188    }
189
190    /// Auto-connect to an agent, attempting to detect available transports.
191    ///
192    /// Probes for WebSocket support by fetching the agent card from the server.
193    /// Falls back to HTTP-only if agent card fetching fails or if WebSocket
194    /// is not supported.
195    ///
196    /// # Arguments
197    ///
198    /// * `base_url` - Base URL of the A2A agent
199    ///
200    /// # Examples
201    ///
202    /// ```rust,no_run
203    /// use a2a_client::WebA2AClient;
204    ///
205    /// # #[tokio::main]
206    /// # async fn main() -> anyhow::Result<()> {
207    /// let client = WebA2AClient::auto_connect("http://localhost:8080").await?;
208    /// # Ok(())
209    /// # }
210    /// ```
211    pub async fn auto_connect(base_url: &str) -> Result<Self> {
212        // Validate URL format
213        let _ = reqwest::Url::parse(base_url).map_err(|e| ClientError::InvalidUrl {
214            url: base_url.to_string(),
215            reason: e.to_string(),
216        })?;
217
218        Ok(Self::new_http(base_url.to_string()))
219    }
220}
221/// Application state for Axum web applications.
222///
223/// This struct provides a convenient way to share the A2A client and
224/// configuration across Axum route handlers.
225///
226/// # Examples
227///
228/// ```rust
229/// use a2a_client::{WebA2AClient, AppState};
230/// use std::sync::Arc;
231///
232/// let client = WebA2AClient::new_http("http://localhost:8080".to_string());
233/// let state = Arc::new(
234///     AppState::new(client)
235///         .with_webhook_token("secret-token".to_string())
236/// );
237/// ```
238pub struct AppState {
239    /// The A2A client for interacting with agents
240    pub client: WebA2AClient,
241    /// Optional webhook authentication token
242    pub webhook_token: Option<String>,
243}
244
245impl AppState {
246    /// Create new application state with the given client.
247    ///
248    /// # Examples
249    ///
250    /// ```rust
251    /// use a2a_client::{WebA2AClient, AppState};
252    ///
253    /// let client = WebA2AClient::new_http("http://localhost:8080".to_string());
254    /// let state = AppState::new(client);
255    /// ```
256    pub fn new(client: WebA2AClient) -> Self {
257        Self {
258            client,
259            webhook_token: None,
260        }
261    }
262
263    /// Set the webhook authentication token.
264    ///
265    /// # Examples
266    ///
267    /// ```rust
268    /// use a2a_client::{WebA2AClient, AppState};
269    ///
270    /// let client = WebA2AClient::new_http("http://localhost:8080".to_string());
271    /// let state = AppState::new(client)
272    ///     .with_webhook_token("secret-token".to_string());
273    /// ```
274    pub fn with_webhook_token(mut self, token: String) -> Self {
275        self.webhook_token = Some(token);
276        self
277    }
278}
279
280/// Builder for [`WebA2AClient`].
281///
282/// Provides a fluent API for configuring the client with optional WebSocket support.
283///
284/// # Examples
285///
286/// ```rust
287/// use a2a_client::WebA2AClient;
288///
289/// // HTTP-only client
290/// let client = WebA2AClient::builder()
291///     .http_url("http://localhost:8080")
292///     .build();
293/// ```
294#[derive(Default)]
295pub struct WebA2AClientBuilder {
296    http_url: Option<String>,
297}
298
299impl WebA2AClientBuilder {
300    /// Set the HTTP base URL.
301    ///
302    /// # Examples
303    ///
304    /// ```rust
305    /// use a2a_client::WebA2AClient;
306    ///
307    /// let client = WebA2AClient::builder()
308    ///     .http_url("http://localhost:8080")
309    ///     .build();
310    /// ```
311    pub fn http_url(mut self, url: impl Into<String>) -> Self {
312        self.http_url = Some(url.into());
313        self
314    }
315
316    /// Build the [`WebA2AClient`].
317    ///
318    /// # Panics
319    ///
320    /// Panics if `http_url` was not set.
321    ///
322    /// # Examples
323    ///
324    /// ```rust
325    /// use a2a_client::WebA2AClient;
326    ///
327    /// let client = WebA2AClient::builder()
328    ///     .http_url("http://localhost:8080")
329    ///     .build();
330    /// ```
331    pub fn build(self) -> WebA2AClient {
332        let http_url = self
333            .http_url
334            .expect("http_url is required for WebA2AClient");
335
336        WebA2AClient::new_http(http_url)
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn test_client_builder_http_only() {
346        let _client = WebA2AClient::builder()
347            .http_url("http://test.local")
348            .build();
349    }
350
351    #[test]
352    #[should_panic(expected = "http_url is required for WebA2AClient")]
353    fn test_client_builder_panics_without_http() {
354        let _client = WebA2AClient::builder().build();
355    }
356
357    #[test]
358    fn test_app_state_creation() {
359        let client = WebA2AClient::new_http("http://test.local".to_string());
360        let state = AppState::new(client);
361        assert!(state.webhook_token.is_none());
362    }
363
364    #[test]
365    fn test_app_state_with_token() {
366        let client = WebA2AClient::new_http("http://test.local".to_string());
367        let state = AppState::new(client).with_webhook_token("secret".to_string());
368        assert_eq!(state.webhook_token.as_deref(), Some("secret"));
369    }
370
371    #[tokio::test]
372    async fn test_auto_connect_invalid_url() {
373        let result = WebA2AClient::auto_connect("invalid-url-no-scheme").await;
374        assert!(result.is_err());
375        match result {
376            Err(ClientError::InvalidUrl { url, reason }) => {
377                assert_eq!(url, "invalid-url-no-scheme");
378                assert!(!reason.is_empty());
379            }
380            _ => panic!("Expected ClientError::InvalidUrl"),
381        }
382    }
383}