Quick Start

This guide gets you running the built-in echo agent example in under 5 minutes. The example demonstrates the full A2A stack: agent executor, dual-transport server, and client — all in a single binary.

Running the Echo Agent

Clone the repository and run the example:

git clone https://github.com/tomtom215/a2a-rust.git
cd a2a-rust
cargo run -p echo-agent

You'll see output like:

=== A2A Echo Agent Example ===

JSON-RPC server listening on http://127.0.0.1:54321
REST server listening on http://127.0.0.1:54322

--- Demo 1: Synchronous SendMessage (JSON-RPC) ---
  Task ID:    550e8400-e29b-41d4-a716-446655440000
  Status:     Completed
  Artifact:   echo-artifact
  Content:    Echo: Hello from JSON-RPC client!

--- Demo 2: Streaming SendMessage (JSON-RPC) ---
  Status update: Working
  Artifact update: echo-artifact
  Content:    Echo: Hello from streaming client!
  Status update: Completed

--- Demo 3: Synchronous SendMessage (REST) ---
  Task ID:    ...
  Status:     Completed
  Content:    Echo: Hello from REST client!

--- Demo 4: Streaming SendMessage (REST) ---
  Status update: Working
  Content:    Echo: Hello from REST streaming!
  Status update: Completed

--- Demo 5: GetTask ---
  Fetched task: ... (Completed)

=== All demos completed successfully! ===

What Just Happened?

The example exercised all major protocol operations:

  1. Synchronous send (JSON-RPC) — Client sends a message, waits for the complete task
  2. Streaming send (JSON-RPC) — Client receives real-time SSE events as the agent works
  3. Synchronous send (REST) — Same operation over the REST transport
  4. Streaming send (REST) — SSE streaming over REST
  5. GetTask — Retrieves a previously completed task by ID

The Code in Brief

The echo agent is about 100 lines of Rust. Here's the core executor:

#![allow(unused)]
fn main() {
use a2a_protocol_sdk::prelude::*;
use std::future::Future;
use std::pin::Pin;

struct EchoExecutor;

impl AgentExecutor for EchoExecutor {
    fn execute<'a>(
        &'a self,
        ctx: &'a RequestContext,
        queue: &'a dyn EventQueueWriter,
    ) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
        Box::pin(async move {
            // 1. Transition to Working
            queue.write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
                task_id: ctx.task_id.clone(),
                context_id: ContextId::new(ctx.context_id.clone()),
                status: TaskStatus::new(TaskState::Working),
                metadata: None,
            })).await?;

            // 2. Extract text from incoming message
            let input = ctx.message.parts.iter()
                .find_map(|p| match &p.content {
                    a2a_protocol_types::message::PartContent::Text { text } => Some(text.as_str()),
                    _ => None,
                })
                .unwrap_or("<no text>");

            // 3. Echo back as an artifact
            queue.write(StreamResponse::ArtifactUpdate(TaskArtifactUpdateEvent {
                task_id: ctx.task_id.clone(),
                context_id: ContextId::new(ctx.context_id.clone()),
                artifact: Artifact::new("echo-artifact", vec![Part::text(
                    &format!("Echo: {input}")
                )]),
                append: None,
                last_chunk: Some(true),
                metadata: None,
            })).await?;

            // 4. Transition to Completed
            queue.write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
                task_id: ctx.task_id.clone(),
                context_id: ContextId::new(ctx.context_id.clone()),
                status: TaskStatus::new(TaskState::Completed),
                metadata: None,
            })).await?;

            Ok(())
        })
    }
}
}

The pattern is always: write status updates and artifacts to the event queue, then return Ok(()).

With Tracing

Enable structured logging to see the protocol internals:

cargo run -p echo-agent --features tracing
RUST_LOG=debug cargo run -p echo-agent --features tracing

Next Steps