Jesús Pérez b6a4d77421
Some checks are pending
Documentation Lint & Validation / Markdown Linting (push) Waiting to run
Documentation Lint & Validation / Validate mdBook Configuration (push) Waiting to run
Documentation Lint & Validation / Content & Structure Validation (push) Waiting to run
Documentation Lint & Validation / Lint & Validation Summary (push) Blocked by required conditions
mdBook Build & Deploy / Build mdBook (push) Waiting to run
mdBook Build & Deploy / Documentation Quality Check (push) Blocked by required conditions
mdBook Build & Deploy / Deploy to GitHub Pages (push) Blocked by required conditions
mdBook Build & Deploy / Notification (push) Blocked by required conditions
Rust CI / Security Audit (push) Waiting to run
Rust CI / Check + Test + Lint (nightly) (push) Waiting to run
Rust CI / Check + Test + Lint (stable) (push) Waiting to run
feat: add Leptos UI library and modularize MCP server
2026-02-14 20:10:55 +00:00

351 lines
7.7 KiB
Markdown

# 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<dyn std::error::Error>> {
// 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<AgentCard>`
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<String>`
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<A2aTaskStatus>`
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<bool>`
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