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, SendCompletion};
26//! use a2a_rs::Transport;
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//! // `WhenSettled` is the A2A default: the server holds the response until the
37//! // task finishes, so `task` carries the agent's reply rather than an ack.
38//! let task = client
39//! .transport
40//! .send_task_message("task-1", &message, None, None, SendCompletion::WhenSettled)
41//! .await?;
42//! println!("Task ID: {}", task.id);
43//! # Ok(())
44//! # }
45//! ```
46//!
47//! ### With SSE Streaming via ConnectRPC
48//!
49//! ```rust
50//! use a2a_client::WebA2AClient;
51//!
52//! // Create client with HTTP (which handles ConnectRPC streaming automatically)
53//! let client = WebA2AClient::new_http("http://localhost:8080".to_string());
54//!
55//! println!("HTTP client configured for streaming!");
56//! ```
57//!
58//! ### SSE Streaming with Axum (requires `axum-components` feature)
59//!
60//! ```rust,ignore
61//! # #[cfg(feature = "axum-components")]
62//! # {
63//! use a2a_client::{WebA2AClient, components::create_sse_stream};
64//! use axum::{Router, routing::get, extract::{State, Path}};
65//! use std::sync::Arc;
66//!
67//! # #[tokio::main]
68//! # async fn main() -> anyhow::Result<()> {
69//! let client = Arc::new(WebA2AClient::new_http("http://localhost:8080".to_string()));
70//!
71//! let app = Router::new()
72//! .route("/stream/:task_id", get(stream_handler))
73//! .with_state(client);
74//!
75//! // Start your Axum server...
76//! # Ok(())
77//! # }
78//!
79//! async fn stream_handler(
80//! State(client): State<Arc<WebA2AClient>>,
81//! Path(task_id): Path<String>,
82//! ) -> axum::response::sse::Sse<impl futures::Stream<Item = Result<axum::response::sse::Event, std::convert::Infallible>>> {
83//! create_sse_stream(client, task_id)
84//! }
85//! # }
86//! ```
87//!
88//! ## Components
89//!
90//! - [`WebA2AClient`] - Main client wrapper for HTTP and WebSocket transports
91//! - [`components::TaskView`] - View model for displaying tasks in lists
92//! - [`components::MessageView`] - View model for displaying individual messages
93//! - [`components::create_sse_stream`] - SSE stream creation with auto-fallback (requires `axum-components`)
94//! - [`utils::formatters`] - Formatting utilities for A2A types
95//!
96//! ## Feature Flags
97//!
98//! - `axum-components` (default) - Enables Axum-specific SSE streaming components
99//!
100//! ## Integration
101//!
102//! This library integrates with:
103//! - [`a2a-rs`](https://docs.rs/a2a-rs) - Core A2A protocol implementation
104//! - [`a2a-agents`](https://docs.rs/a2a-agents) - Declarative agent framework
105//! - Any agent implementing the A2A Protocol v1.0.0
106//!
107//! ## Examples
108//!
109//! See the `examples/` directory for complete working examples of different use cases.
110
111pub mod components;
112pub mod error;
113pub mod utils;
114
115// Re-export commonly used types
116pub use error::{ClientError, Result};
117
118use std::pin::Pin;
119use std::sync::Arc;
120
121use a2a_rs::domain::A2AError;
122use a2a_rs::{HttpClient, RetryPolicy, StreamEvent, Transport, subscribe_resilient};
123use futures::Stream;
124
125/// Web-friendly A2A client that wraps both HTTP and WebSocket clients.
126///
127/// This is the main entry point for interacting with A2A agents from web applications.
128/// It provides a unified interface for both HTTP and WebSocket transports, with automatic
129/// fallback and retry logic.
130///
131/// # Examples
132///
133/// ## HTTP-only client
134///
135/// ```rust
136/// use a2a_client::WebA2AClient;
137///
138/// let client = WebA2AClient::new_http("http://localhost:8080".to_string());
139/// ```
140///
141/// ## Client configured for streaming
142///
143/// ```rust
144/// use a2a_client::WebA2AClient;
145///
146/// let client = WebA2AClient::new_http("http://localhost:8080".to_string());
147/// ```
148///
149/// ## Auto-detecting transports
150///
151/// ```rust,no_run
152/// use a2a_client::WebA2AClient;
153///
154/// # #[tokio::main]
155/// # async fn main() -> anyhow::Result<()> {
156/// let client = WebA2AClient::auto_connect("http://localhost:8080").await?;
157/// # Ok(())
158/// # }
159/// ```
160pub struct WebA2AClient {
161 /// The negotiated transport for A2A requests and streaming.
162 ///
163 /// Held behind an `Arc<dyn Transport>` so the client is agnostic to the
164 /// underlying wire protocol (ConnectRPC, JSON-RPC 2.0, …) and can share the
165 /// transport with a reconnecting subscription stream.
166 pub transport: Arc<dyn Transport>,
167}
168
169impl WebA2AClient {
170 /// Create a builder for configuring the client.
171 ///
172 /// # Examples
173 ///
174 /// ```rust
175 /// use a2a_client::WebA2AClient;
176 ///
177 /// let client = WebA2AClient::builder()
178 /// .http_url("http://localhost:8080")
179 /// .build();
180 /// ```
181 pub fn builder() -> WebA2AClientBuilder {
182 WebA2AClientBuilder::default()
183 }
184
185 /// Create a new client with HTTP transport only.
186 ///
187 /// # Arguments
188 ///
189 /// * `base_url` - Base URL of the A2A agent (e.g., `http://localhost:8080`)
190 ///
191 /// # Examples
192 ///
193 /// ```rust
194 /// use a2a_client::WebA2AClient;
195 ///
196 /// let client = WebA2AClient::new_http("http://localhost:8080".to_string());
197 /// ```
198 pub fn new_http(base_url: String) -> Self {
199 Self {
200 transport: Arc::new(HttpClient::new(base_url)),
201 }
202 }
203
204 /// Auto-connect to an agent by fetching its card and negotiating a transport.
205 ///
206 /// Fetches the agent card from the well-known endpoint and selects a transport
207 /// from the card's `supported_interfaces` (ConnectRPC preferred, JSON-RPC 2.0
208 /// as interop fallback). If the card can't be fetched or none of its interfaces
209 /// match a compiled-in transport, falls back to a ConnectRPC client on
210 /// `base_url` so the call still works against a bare agent URL.
211 ///
212 /// # Arguments
213 ///
214 /// * `base_url` - Base URL of the A2A agent
215 ///
216 /// # Examples
217 ///
218 /// ```rust,no_run
219 /// use a2a_client::WebA2AClient;
220 ///
221 /// # #[tokio::main]
222 /// # async fn main() -> anyhow::Result<()> {
223 /// let client = WebA2AClient::auto_connect("http://localhost:8080").await?;
224 /// # Ok(())
225 /// # }
226 /// ```
227 pub async fn auto_connect(base_url: &str) -> Result<Self> {
228 // Delegates to the shared `a2a_rs::auto_connect` (URL-validate → negotiate
229 // → direct-client fallback); the CLI and this web client are siblings on
230 // that one entry point.
231 let transport = a2a_rs::auto_connect(base_url).await?;
232 Ok(Self {
233 transport: Arc::from(transport),
234 })
235 }
236
237 /// Subscribe to a task's updates as a protocol-neutral stream of
238 /// [`StreamEvent`]s.
239 ///
240 /// This is the **spec-compliant** path: a single A2A `SubscribeToTask` round
241 /// trip with no reconnection and no `Last-Event-ID` — what any A2A agent
242 /// expects. For automatic reconnection (and gap-free resume against an
243 /// a2a-rs server) use
244 /// [`subscribe_resilient`](WebA2AClient::subscribe_resilient).
245 pub async fn subscribe(
246 &self,
247 task_id: &str,
248 ) -> Result<Pin<Box<dyn Stream<Item = std::result::Result<StreamEvent, A2AError>> + Send>>>
249 {
250 self.transport
251 .subscribe_to_task(task_id, None, None)
252 .await
253 .map_err(Into::into)
254 }
255
256 /// Subscribe to a task's updates with automatic reconnect + exponential
257 /// backoff. The stream ends when the task reaches a terminal state (or
258 /// retries are exhausted).
259 ///
260 /// Reconnection itself is spec-compliant (it re-issues `SubscribeToTask`).
261 /// Resuming *without gaps* via `Last-Event-ID` is an **a2a-rs enhancement**
262 /// beyond the A2A v1.0 spec: it works against an a2a-rs server and degrades
263 /// gracefully (reconnect-from-current-state) against any spec-compliant one.
264 ///
265 /// This is the reusable core that [`create_sse_stream`](components::create_sse_stream)
266 /// builds on; framework-agnostic, so non-Axum frontends can consume it
267 /// directly.
268 pub fn subscribe_resilient(
269 &self,
270 task_id: &str,
271 policy: RetryPolicy,
272 ) -> Pin<Box<dyn Stream<Item = std::result::Result<StreamEvent, A2AError>> + Send>> {
273 subscribe_resilient(
274 self.transport.clone(),
275 task_id.to_string(),
276 None,
277 None,
278 policy,
279 )
280 }
281}
282/// Application state for Axum web applications.
283///
284/// This struct provides a convenient way to share the A2A client and
285/// configuration across Axum route handlers.
286///
287/// # Examples
288///
289/// ```rust
290/// use a2a_client::{WebA2AClient, AppState};
291/// use std::sync::Arc;
292///
293/// let client = WebA2AClient::new_http("http://localhost:8080".to_string());
294/// let state = Arc::new(
295/// AppState::new(client)
296/// .with_webhook_token("secret-token".to_string())
297/// );
298/// ```
299pub struct AppState {
300 /// The A2A client for interacting with agents
301 pub client: WebA2AClient,
302 /// Optional webhook authentication token
303 pub webhook_token: Option<String>,
304}
305
306impl AppState {
307 /// Create new application state with the given client.
308 ///
309 /// # Examples
310 ///
311 /// ```rust
312 /// use a2a_client::{WebA2AClient, AppState};
313 ///
314 /// let client = WebA2AClient::new_http("http://localhost:8080".to_string());
315 /// let state = AppState::new(client);
316 /// ```
317 pub fn new(client: WebA2AClient) -> Self {
318 Self {
319 client,
320 webhook_token: None,
321 }
322 }
323
324 /// Set the webhook authentication token.
325 ///
326 /// # Examples
327 ///
328 /// ```rust
329 /// use a2a_client::{WebA2AClient, AppState};
330 ///
331 /// let client = WebA2AClient::new_http("http://localhost:8080".to_string());
332 /// let state = AppState::new(client)
333 /// .with_webhook_token("secret-token".to_string());
334 /// ```
335 pub fn with_webhook_token(mut self, token: String) -> Self {
336 self.webhook_token = Some(token);
337 self
338 }
339}
340
341/// Builder for [`WebA2AClient`].
342///
343/// Provides a fluent API for configuring the client with optional WebSocket support.
344///
345/// # Examples
346///
347/// ```rust
348/// use a2a_client::WebA2AClient;
349///
350/// // HTTP-only client
351/// let client = WebA2AClient::builder()
352/// .http_url("http://localhost:8080")
353/// .build();
354/// ```
355#[derive(Default)]
356pub struct WebA2AClientBuilder {
357 http_url: Option<String>,
358}
359
360impl WebA2AClientBuilder {
361 /// Set the HTTP base URL.
362 ///
363 /// # Examples
364 ///
365 /// ```rust
366 /// use a2a_client::WebA2AClient;
367 ///
368 /// let client = WebA2AClient::builder()
369 /// .http_url("http://localhost:8080")
370 /// .build();
371 /// ```
372 pub fn http_url(mut self, url: impl Into<String>) -> Self {
373 self.http_url = Some(url.into());
374 self
375 }
376
377 /// Build the [`WebA2AClient`].
378 ///
379 /// # Panics
380 ///
381 /// Panics if `http_url` was not set.
382 ///
383 /// # Examples
384 ///
385 /// ```rust
386 /// use a2a_client::WebA2AClient;
387 ///
388 /// let client = WebA2AClient::builder()
389 /// .http_url("http://localhost:8080")
390 /// .build();
391 /// ```
392 pub fn build(self) -> WebA2AClient {
393 let http_url = self
394 .http_url
395 .expect("http_url is required for WebA2AClient");
396
397 WebA2AClient::new_http(http_url)
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
406 fn test_client_builder_http_only() {
407 let _client = WebA2AClient::builder()
408 .http_url("http://test.local")
409 .build();
410 }
411
412 #[test]
413 #[should_panic(expected = "http_url is required for WebA2AClient")]
414 fn test_client_builder_panics_without_http() {
415 let _client = WebA2AClient::builder().build();
416 }
417
418 #[test]
419 fn test_app_state_creation() {
420 let client = WebA2AClient::new_http("http://test.local".to_string());
421 let state = AppState::new(client);
422 assert!(state.webhook_token.is_none());
423 }
424
425 #[test]
426 fn test_app_state_with_token() {
427 let client = WebA2AClient::new_http("http://test.local".to_string());
428 let state = AppState::new(client).with_webhook_token("secret".to_string());
429 assert_eq!(state.webhook_token.as_deref(), Some("secret"));
430 }
431
432 #[tokio::test]
433 async fn test_auto_connect_invalid_url() {
434 // URL validation now lives in `a2a_rs::auto_connect`, so a malformed URL
435 // surfaces as an `A2AError::InvalidParams` wrapped in `ClientError::A2AError`.
436 // `WebA2AClient` isn't `Debug`, so discard the Ok value before matching.
437 let err = WebA2AClient::auto_connect("invalid-url-no-scheme")
438 .await
439 .err()
440 .expect("malformed url should error");
441 match err {
442 ClientError::A2AError(A2AError::InvalidParams(msg)) => {
443 assert!(msg.contains("invalid-url-no-scheme"), "got {msg}");
444 }
445 other => panic!("Expected ClientError::A2AError(InvalidParams), got {other:?}"),
446 }
447 }
448}