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
TaskStoreplusInMemoryTaskStore
This crate has zero Clawhive-specific logic.
- 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.
| 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 }Add the crate:
[dependencies] a2a-rust = "0.1"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 serverUse 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 clientGET /.well-known/agent-card.json
- server default endpoint:
POST /rpc - compatibility alias:
POST /jsonrpc - method names use PascalCase v1.0 bindings such as
SendMessage,GetTask, andListTasks
Canonical REST endpoints include:
POST /message:sendPOST /message:streamGET /tasksGET /tasks/{id}POST /tasks/{id}:cancelGET /tasks/{id}:subscribePOST /tasks/{task_id}/pushNotificationConfigsGET /tasks/{task_id}/pushNotificationConfigs/{id}GET /tasks/{task_id}/pushNotificationConfigsDELETE /tasks/{task_id}/pushNotificationConfigs/{id}GET /extendedAgentCard
Tenant-prefixed variants are also supported.
- Discovery caches agent cards with a configurable TTL
- Transport selection follows the server-declared
supported_interfacesorder - Supported transports:
JSONRPC,HTTP+JSON - Streaming uses SSE and parses both
\n\nand\r\n\r\nframe delimiters A2A-Version: 1.0is always sent
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 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-featuresSee CONTRIBUTING.md for contributor workflow details.
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT License (LICENSE-MIT)
at your option.