Skip to content

longzhi/a2a-rust

a2a-rust

CI Crates.io docs.rs License

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.

About

Rust SDK for Google A2A (Agent-to-Agent) Protocol v1.0 — types, server framework (axum), and client library

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages