a2a-rust

Rust SDK for A2A Protocol v1.0.
a2a-rust provides:
- a proto-aligned type layer
- an axum-based server with REST, JSON-RPC, and SSE
- a reqwest-based client with discovery, dual transport, and SSE parsing
- a pluggable
TaskStore plus InMemoryTaskStore
This crate has zero Clawhive-specific logic.
Status
- Protocol lock:
v1.0.0
- Proto package:
lf.a2a.v1
- Implemented transports:
JSONRPC, HTTP+JSON
- Out of scope: gRPC
The tagged proto is the source of truth. The repo-local implementation contract is docs/proto-first-design.md.
Features
| Feature |
Default |
Purpose |
server |
Yes |
Router, handlers, SSE, and TaskStore support |
client |
Yes |
Discovery, dual transport client, and SSE parsing |
Types-only usage:
[dependencies]
a2a-rust = { version = "0.1", default-features = false }
Quick Start
Add the crate:
[dependencies]
a2a-rust = "0.1"
Server
Implement A2AHandler and mount the router:
use a2a_rust::server::{A2AHandler, router};
use a2a_rust::types::{
AgentCapabilities, AgentCard, AgentInterface, Message, Part, Role, SendMessageRequest,
SendMessageResponse,
};
use a2a_rust::A2AError;
#[derive(Clone)]
struct EchoAgent;
#[async_trait::async_trait]
impl A2AHandler for EchoAgent {
async fn get_agent_card(&self) -> Result<AgentCard, A2AError> {
Ok(AgentCard {
name: "Echo Agent".to_owned(),
description: "Replies with the same text".to_owned(),
supported_interfaces: vec![
AgentInterface {
url: "/rpc".to_owned(),
protocol_binding: "JSONRPC".to_owned(),
tenant: None,
protocol_version: "1.0".to_owned(),
},
AgentInterface {
url: "/".to_owned(),
protocol_binding: "HTTP+JSON".to_owned(),
tenant: None,
protocol_version: "1.0".to_owned(),
},
],
provider: None,
version: "1.0.0".to_owned(),
documentation_url: None,
capabilities: AgentCapabilities {
streaming: Some(false),
push_notifications: Some(false),
extensions: Vec::new(),
extended_agent_card: Some(false),
},
security_schemes: Default::default(),
security_requirements: Vec::new(),
default_input_modes: vec!["text/plain".to_owned()],
default_output_modes: vec!["text/plain".to_owned()],
skills: Vec::new(),
signatures: Vec::new(),
icon_url: None,
})
}
async fn send_message(
&self,
request: SendMessageRequest,
) -> Result<SendMessageResponse, A2AError> {
Ok(SendMessageResponse::Message(Message {
message_id: "msg-echo-1".to_owned(),
context_id: request.message.context_id,
task_id: None,
role: Role::Agent,
parts: vec![Part {
text: Some("pong".to_owned()),
raw: None,
url: None,
data: None,
metadata: None,
filename: None,
media_type: None,
}],
metadata: None,
extensions: Vec::new(),
reference_task_ids: Vec::new(),
}))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
axum::serve(listener, router(EchoAgent)).await?;
Ok(())
}
Runnable example:
cargo run --example echo_server --features server
Client
Use discovery and send a message:
use a2a_rust::client::A2AClient;
use a2a_rust::types::{Message, Part, Role, SendMessageRequest, SendMessageResponse};
#[tokio::main]
async fn main() -> Result<(), a2a_rust::A2AError> {
let client = A2AClient::new("http://127.0.0.1:3000")?;
let card = client.discover_agent_card().await?;
let response = client
.send_message(SendMessageRequest {
message: Message {
message_id: "msg-1".to_owned(),
context_id: Some("ctx-1".to_owned()),
task_id: None,
role: Role::User,
parts: vec![Part {
text: Some("ping".to_owned()),
raw: None,
url: None,
data: None,
metadata: None,
filename: None,
media_type: None,
}],
metadata: None,
extensions: Vec::new(),
reference_task_ids: Vec::new(),
},
configuration: None,
metadata: None,
tenant: None,
})
.await?;
println!("agent: {}", card.name);
match response {
SendMessageResponse::Message(message) => {
println!("reply: {:?}", message.parts[0].text);
}
SendMessageResponse::Task(task) => {
println!("task: {}", task.id);
}
}
Ok(())
}
Runnable example:
cargo run --example ping_client --features client
Protocol Surface
Discovery
GET /.well-known/agent-card.json
JSON-RPC
- server default endpoint:
POST /rpc
- compatibility alias:
POST /jsonrpc
- method names use PascalCase v1.0 bindings such as
SendMessage, GetTask, and ListTasks
REST
Canonical REST endpoints include:
POST /message:send
POST /message:stream
GET /tasks
GET /tasks/{id}
POST /tasks/{id}:cancel
GET /tasks/{id}:subscribe
POST /tasks/{task_id}/pushNotificationConfigs
GET /tasks/{task_id}/pushNotificationConfigs/{id}
GET /tasks/{task_id}/pushNotificationConfigs
DELETE /tasks/{task_id}/pushNotificationConfigs/{id}
GET /extendedAgentCard
Tenant-prefixed variants are also supported.
Client Behavior
- Discovery caches agent cards with a configurable TTL
- Transport selection follows the server-declared
supported_interfaces order
- Supported transports:
JSONRPC, HTTP+JSON
- Streaming uses SSE and parses both
\n\n and \r\n\r\n frame delimiters
A2A-Version: 1.0 is always sent
Project Layout
src/
lib.rs
error.rs
jsonrpc.rs
store.rs
types/
server/
client/
examples/
echo_server.rs
ping_client.rs
tests/
server_integration.rs
client_integration.rs
client_wiremock.rs
Development
Core checks:
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo clippy --all-targets --no-default-features -- -D warnings
cargo test --all-features
cargo test --no-default-features
See CONTRIBUTING.md for contributor workflow details.
References
License
Licensed under either of:
at your option.