# vapora-a2a-client **A2A Protocol Client** - Resilient HTTP client for calling Agent-to-Agent (A2A) protocol servers. ## Features - ✅ **Full A2A Protocol Support** - Discovery, dispatch, status query - ✅ **Exponential Backoff Retry** - Configurable retry policy with jitter - ✅ **Smart Error Handling** - Retries 5xx/network, skips 4xx - ✅ **Type-Safe** - Rust compile-time guarantees - ✅ **Async/Await** - Built on Tokio and Reqwest - ✅ **Comprehensive Tests** - 5 unit tests, all passing ## Quick Start ### Basic Usage ```rust use vapora_a2a_client::A2aClient; #[tokio::main] async fn main() -> Result<(), Box> { // Create client let client = A2aClient::new("http://localhost:8003"); // Discover agent capabilities let agent_card = client.discover_agent().await?; println!("Connected to: {} v{}", agent_card.name, agent_card.version); // Dispatch task let task_id = client.dispatch_task( uuid::Uuid::new_v4().to_string(), "Write hello world function".to_string(), Some("In Rust with tests".to_string()), Some("developer".to_string()), ).await?; println!("Task dispatched: {}", task_id); // Query status let status = client.get_task_status(&task_id).await?; println!("Status: {:?}", status); Ok(()) } ``` ### With Custom Timeout ```rust use std::time::Duration; use vapora_a2a_client::A2aClient; let client = A2aClient::with_timeout( "http://localhost:8003", Duration::from_secs(60), ); ``` ### With Custom Retry Policy ```rust use vapora_a2a_client::{A2aClient, RetryPolicy}; use std::time::Duration; let retry_policy = RetryPolicy { max_retries: 5, initial_delay_ms: 200, max_delay_ms: 10000, backoff_multiplier: 2.0, jitter: true, }; let client = A2aClient::with_retry_policy( "http://localhost:8003", Duration::from_secs(30), retry_policy, ); ``` ## Retry Policy ### How It Works The client automatically retries transient failures using exponential backoff: ``` Attempt 1: Fail (timeout) Wait: 100ms (± 20% jitter) Attempt 2: Fail (5xx error) Wait: 200ms (± 20% jitter) Attempt 3: Success ``` ### Retryable Errors **Retries (up to max_retries):** - Network timeouts - Connection refused - 5xx server errors (500-599) - Connection reset **No Retry (fails immediately):** - 4xx client errors (400-499) - Task not found (404) - Deserialization errors - Invalid response format ### Configuration ```rust pub struct RetryPolicy { pub max_retries: u32, // Default: 3 pub initial_delay_ms: u64, // Default: 100ms pub max_delay_ms: u64, // Default: 5000ms pub backoff_multiplier: f64, // Default: 2.0 pub jitter: bool, // Default: true (±20%) } ``` **Formula:** ``` delay = min(initial_delay * (multiplier ^ attempt), max_delay) if jitter: delay *= random(0.8..1.2) ``` ## API Reference ### Client Creation ```rust // Default timeout (30s), default retry policy let client = A2aClient::new("http://localhost:8003"); // Custom timeout let client = A2aClient::with_timeout( "http://localhost:8003", Duration::from_secs(60), ); // Custom retry policy let client = A2aClient::with_retry_policy( "http://localhost:8003", Duration::from_secs(30), RetryPolicy::default(), ); ``` ### Methods #### `discover_agent() -> Result` Fetches agent capabilities from `/.well-known/agent.json`: ```rust let agent_card = client.discover_agent().await?; println!("Name: {}", agent_card.name); println!("Version: {}", agent_card.version); println!("Skills: {:?}", agent_card.skills); ``` #### `dispatch_task(...) -> Result` Dispatches a task to the A2A server: ```rust let task_id = client.dispatch_task( "task-123".to_string(), // task_id (UUID recommended) "Task title".to_string(), // title Some("Description".to_string()), // description (optional) Some("developer".to_string()), // skill (optional) ).await?; ``` #### `get_task_status(task_id: &str) -> Result` Queries task status: ```rust let status = client.get_task_status("task-123").await?; match status.state.as_str() { "waiting" => println!("Task queued"), "working" => println!("Task in progress"), "completed" => println!("Result: {:?}", status.result), "failed" => println!("Error: {:?}", status.error), _ => {} } ``` #### `health_check() -> Result` Checks server health: ```rust if client.health_check().await? { println!("Server healthy"); } ``` ## Error Handling ```rust use vapora_a2a_client::{A2aClient, A2aClientError}; match client.dispatch_task(...).await { Ok(task_id) => println!("Success: {}", task_id), Err(A2aClientError::Timeout(url)) => { eprintln!("Timeout connecting to: {}", url); } Err(A2aClientError::ConnectionRefused(url)) => { eprintln!("Connection refused: {}", url); } Err(A2aClientError::ServerError { code, message }) => { eprintln!("Server error {}: {}", code, message); } Err(A2aClientError::TaskNotFound(id)) => { eprintln!("Task not found: {}", id); } Err(e) => eprintln!("Other error: {}", e), } ``` ## Testing ```bash # Run all tests cargo test -p vapora-a2a-client # Output: # test retry::tests::test_retry_succeeds_eventually ... ok # test retry::tests::test_retry_exhausted ... ok # test retry::tests::test_non_retryable_error ... ok # test client::tests::test_client_creation ... ok # test client::tests::test_client_with_custom_timeout ... ok # # test result: ok. 5 passed; 0 failed; 0 ignored ``` ## Examples ### Polling for Completion ```rust use tokio::time::{sleep, Duration}; let task_id = client.dispatch_task(...).await?; loop { let status = client.get_task_status(&task_id).await?; match status.state.as_str() { "completed" => { println!("Success: {:?}", status.result); break; } "failed" => { eprintln!("Failed: {:?}", status.error); break; } _ => { println!("Status: {}", status.state); sleep(Duration::from_millis(500)).await; } } } ``` ### Batch Task Dispatch ```rust use futures::future::join_all; let tasks = vec!["Task 1", "Task 2", "Task 3"]; let futures = tasks.iter().map(|title| { client.dispatch_task( uuid::Uuid::new_v4().to_string(), title.to_string(), None, Some("developer".to_string()), ) }); let task_ids = join_all(futures).await; for result in task_ids { match result { Ok(id) => println!("Dispatched: {}", id), Err(e) => eprintln!("Failed: {}", e), } } ``` ### Custom Retry Logic ```rust use vapora_a2a_client::{RetryPolicy, A2aClient}; use std::time::Duration; // Conservative retry: fewer attempts, longer delays let conservative = RetryPolicy { max_retries: 2, initial_delay_ms: 500, max_delay_ms: 10000, backoff_multiplier: 3.0, jitter: true, }; // Aggressive retry: more attempts, shorter delays let aggressive = RetryPolicy { max_retries: 10, initial_delay_ms: 50, max_delay_ms: 2000, backoff_multiplier: 1.5, jitter: true, }; let client = A2aClient::with_retry_policy( "http://localhost:8003", Duration::from_secs(30), conservative, ); ``` ## Dependencies ```toml [dependencies] vapora-a2a = { workspace = true } reqwest = { workspace = true, features = ["json"] } tokio = { workspace = true, features = ["full"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } uuid = { workspace = true, features = ["v4", "serde"] } rand = { workspace = true } ``` ## Related Crates - **vapora-a2a** - Server implementation - **vapora-agents** - Agent coordinator ## License MIT OR Apache-2.0