a2a-rust 0.1.0

Rust SDK for the A2A (Agent-to-Agent) protocol
Documentation

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.