# brainwires-a2a
Full Rust implementation of the [Agent-to-Agent (A2A)](https://github.com/a2a-protocol/a2a) protocol — the open standard (Google / Linux Foundation) for interoperable agent communication.
Covers all three protocol bindings: **JSON-RPC 2.0**, **HTTP/REST**, and **gRPC**.
## Features
| `client` | yes (via `native`) | HTTP client for JSON-RPC and REST (reqwest) |
| `server` | yes (via `native`) | HTTP server for JSON-RPC and REST (hyper) |
| `native` | **yes** | Both `client` and `server` |
| `grpc` | no | Proto types (prost + tonic) |
| `grpc-client` | no | gRPC client transport |
| `grpc-server` | no | gRPC server service |
| `full` | no | Everything |
Types are always available with no features enabled — useful if you only need the data model.
## Quick start
### Client
```rust
use brainwires_a2a::{A2aClient, Message, SendMessageRequest};
use url::Url;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Discover the agent
let card = A2aClient::discover("https://agent.example.com").await?;
println!("Connected to: {}", card.name);
// Create a client (JSON-RPC is the primary binding)
let client = A2aClient::new_jsonrpc(Url::parse("https://agent.example.com")?);
// Send a message
let response = client.send_message(SendMessageRequest {
tenant: None,
message: Message::user_text("Hello, agent!"),
configuration: None,
metadata: None,
}).await?;
println!("{response:?}");
Ok(())
}
```
### Server
Implement the `A2aHandler` trait once — the server routes JSON-RPC, REST, and gRPC requests to it automatically.
```rust
use std::net::SocketAddr;
use std::pin::Pin;
use async_trait::async_trait;
use brainwires_a2a::*;
use futures::Stream;
struct MyAgent { card: AgentCard }
#[async_trait]
impl A2aHandler for MyAgent {
fn agent_card(&self) -> &AgentCard { &self.card }
async fn on_send_message(
&self,
req: SendMessageRequest,
) -> Result<SendMessageResponse, A2aError> {
let task = Task {
id: uuid::Uuid::new_v4().to_string(),
context_id: req.message.context_id.clone(),
status: TaskStatus {
state: TaskState::Completed,
message: Some(Message::agent_text("Hello back!")),
timestamp: None,
},
artifacts: None,
history: Some(vec![req.message]),
metadata: None,
kind: "task".into(),
};
Ok(SendMessageResponse::Task(task))
}
async fn on_send_streaming_message(
&self,
_req: SendMessageRequest,
) -> Result<Pin<Box<dyn Stream<Item = Result<StreamEvent, A2aError>> + Send>>, A2aError> {
Err(A2aError::unsupported_operation("streaming"))
}
async fn on_get_task(&self, req: GetTaskRequest) -> Result<Task, A2aError> {
Err(A2aError::task_not_found(&req.id))
}
async fn on_list_tasks(&self, _req: ListTasksRequest) -> Result<ListTasksResponse, A2aError> {
Ok(ListTasksResponse {
tasks: vec![],
next_page_token: String::new(),
page_size: 0,
total_size: 0,
})
}
async fn on_cancel_task(&self, req: CancelTaskRequest) -> Result<Task, A2aError> {
Err(A2aError::task_not_found(&req.id))
}
async fn on_subscribe_to_task(
&self,
req: SubscribeToTaskRequest,
) -> Result<Pin<Box<dyn Stream<Item = Result<StreamEvent, A2aError>> + Send>>, A2aError> {
Err(A2aError::task_not_found(&req.id))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let card = AgentCard {
name: "My Agent".into(),
description: "Example A2A agent".into(),
version: "1.0.0".into(),
supported_interfaces: None,
capabilities: AgentCapabilities {
streaming: Some(false),
push_notifications: Some(false),
extended_agent_card: None,
extensions: None,
},
skills: vec![AgentSkill {
id: "chat".into(),
name: "Chat".into(),
description: "Basic chat".into(),
tags: vec!["chat".into()],
examples: None,
input_modes: None,
output_modes: None,
security_requirements: None,
}],
default_input_modes: vec!["text/plain".into()],
default_output_modes: vec!["text/plain".into()],
provider: None,
security_schemes: None,
security_requirements: None,
documentation_url: None,
icon_url: None,
signatures: None,
};
let addr: SocketAddr = "0.0.0.0:8080".parse()?;
let server = A2aServer::new(MyAgent { card }, addr);
server.run().await?;
Ok(())
}
```
## Protocol bindings
### JSON-RPC 2.0 (primary)
The primary binding used by the official Python SDK. Requests are `POST /` with a JSON-RPC body. Streaming methods (`message/stream`, `tasks/resubscribe`) return `text/event-stream` (SSE) where each `data:` line is a JSON-RPC response.
**Methods:**
| `message/send` | Send a message, get Task or Message back |
| `message/stream` | Send a message, stream SSE events |
| `tasks/get` | Get a task by ID |
| `tasks/list` | List tasks with filters |
| `tasks/cancel` | Cancel a running task |
| `tasks/resubscribe` | Re-subscribe to task updates (SSE) |
| `tasks/pushNotificationConfig/set` | Create/update push config |
| `tasks/pushNotificationConfig/get` | Get push config |
| `tasks/pushNotificationConfig/list` | List push configs |
| `tasks/pushNotificationConfig/delete` | Delete push config |
| `agent/authenticatedExtendedCard` | Get extended agent card |
### HTTP/REST
RESTful endpoints derived from `google.api.http` annotations in the proto spec. All endpoints also accept an optional `/{tenant}/` prefix.
| GET | `/.well-known/agent-card.json` | Agent card discovery |
| POST | `/message:send` | Send message |
| POST | `/message:stream` | Stream message (SSE) |
| GET | `/tasks/{id}` | Get task |
| GET | `/tasks` | List tasks |
| POST | `/tasks/{id}:cancel` | Cancel task |
| GET | `/tasks/{id}:subscribe` | Subscribe to updates (SSE) |
| POST | `/tasks/{task_id}/pushNotificationConfigs` | Create push config |
| GET | `/tasks/{task_id}/pushNotificationConfigs/{id}` | Get push config |
| GET | `/tasks/{task_id}/pushNotificationConfigs` | List push configs |
| DELETE | `/tasks/{task_id}/pushNotificationConfigs/{id}` | Delete push config |
| GET | `/extendedAgentCard` | Get extended agent card |
### gRPC
Generated from the official `a2a.proto` (`lf.a2a.v1.A2AService`) via `tonic-build`. Enable with the `grpc`, `grpc-client`, or `grpc-server` features.
The gRPC server runs on a separate port and can be enabled alongside HTTP:
```rust
let server = A2aServer::new(handler, http_addr)
.with_grpc(grpc_addr);
server.run().await?;
```
## Transport selection (client)
```rust
// JSON-RPC (default, compatible with Python SDK)
let client = A2aClient::new_jsonrpc(url);
// REST
let client = A2aClient::new_rest(url);
// gRPC (requires grpc-client feature)
let client = A2aClient::new_grpc("http://localhost:50051").await?;
```
All `A2aClient` methods work identically regardless of transport.
## Error codes
Spec-defined JSON-RPC error codes are available as constants:
| -32700 | `JSON_PARSE_ERROR` | Invalid JSON payload |
| -32600 | `INVALID_REQUEST` | Request validation error |
| -32601 | `METHOD_NOT_FOUND` | Method not found |
| -32602 | `INVALID_PARAMS` | Invalid parameters |
| -32603 | `INTERNAL_ERROR` | Internal error |
| -32001 | `TASK_NOT_FOUND` | Task not found |
| -32002 | `TASK_NOT_CANCELABLE` | Task cannot be canceled |
| -32003 | `PUSH_NOT_SUPPORTED` | Push notifications not supported |
| -32004 | `UNSUPPORTED_OPERATION` | Operation not supported |
| -32005 | `CONTENT_TYPE_NOT_SUPPORTED` | Incompatible content types |
| -32006 | `INVALID_AGENT_RESPONSE` | Invalid agent response |
| -32007 | `EXTENDED_CARD_NOT_CONFIGURED` | Extended card not configured |
## Cargo.toml
```toml
# Types only (no networking)
brainwires-a2a = { version = "0.6", default-features = false }
# Client + server (JSON-RPC + REST)
brainwires-a2a = "0.6"
# Everything including gRPC
brainwires-a2a = { version = "0.6", features = ["full"] }
```
Or via the `brainwires` facade crate:
```toml
brainwires = { version = "0.6", features = ["a2a"] }
```
## License
MIT OR Apache-2.0