Getting Started
This guide walks you through installing agent2agent, building a minimal agent, and calling it from both curl and Ruby.
Installation
gem "agent2agent"
Requires Ruby >= 3.2.
Minimal Agent
A2A.agent builds a complete A2A server (a Rack app) from a single handler block. The block receives the Rack env and routes operations with pattern matching. It returns a schema object (A2A::Protocol::JsonSchema::Definition) — the binding layer formats it into HTTP.
# config.ru
require "a2a"
require "securerandom"
TASKS = {}
agent_card = {
"name" => "Echo Agent",
"url" => "http://localhost:9292",
"version" => "1.0.0",
}
run A2A.agent(agent_card: agent_card) do |env|
case env["a2a.operation"]
in "SendMessage"
text = env["a2a.message"] # extracted text of the message parts
task_id = SecureRandom.uuid
TASKS[task_id] = {
"id" => task_id,
"contextId" => SecureRandom.uuid,
"status" => { "state" => "TASK_STATE_COMPLETED" },
"artifacts" => [{
"artifactId" => SecureRandom.uuid,
"parts" => [{ "text" => "Echo: #{text}" }],
}],
}
A2A::Protocol::JsonSchema["Send Message Response"].new(task: TASKS[task_id])
in "GetTask"
id = env["a2a.request"].id
task = TASKS[id] or raise A2A::TaskNotFoundError.new(id)
A2A::Protocol::JsonSchema["Task"].new(task)
end
end
Any operation the case doesn’t match falls out with NoMatchingPatternError, which the server converts to a spec-compliant UnsupportedOperationError (JSON-RPC code -32004). Raising an A2A::Error subclass (like A2A::TaskNotFoundError above) produces the corresponding protocol error response.
bundle exec falcon serve --bind http://0.0.0.0:9292
The server exposes these endpoint groups automatically:
| Path | Purpose |
|---|---|
/.well-known/agent-card.json |
Agent card discovery |
/ |
JSON-RPC 2.0 binding |
/rest/* |
HTTP+JSON/REST binding |
/grpc |
gRPC binding (reserved — returns 501 Not Implemented) |
Calling It
# Discover
curl http://localhost:9292/.well-known/agent-card.json
# JSON-RPC
curl -X POST http://localhost:9292/ \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":{"messageId":"m1","role":"ROLE_USER","parts":[{"text":"Hello"}]}}}'
# REST
curl -X POST http://localhost:9292/rest/message:send \
-H "Content-Type: application/json" \
-d '{"message":{"messageId":"m1","role":"ROLE_USER","parts":[{"text":"Hello"}]}}'
Client
The client validates request params against the operation’s schema before sending, and returns responses as schema objects (with snake_case readers):
require "a2a"
Async do
client = A2A::Client.new("http://localhost:9292")
card = client.agent_card
card.name # => "Echo Agent"
card.version # => "1.0.0"
result = client.send_message(
message: {
message_id: "msg-1",
role: "ROLE_USER",
parts: [{ text: "Hello" }],
}
)
result.task.status.state # => "TASK_STATE_COMPLETED"
task = client.get_task(id: result.task.id)
end
All 11 protocol operations are available as snake_case methods: send_message, get_task, list_tasks, cancel_task, send_streaming_message, subscribe_to_task, etc. Pass binding: :rest to A2A::Client.new to use the REST binding instead of JSON-RPC.
Mounting in a Host App
An agent doesn’t need its own process — A2A.agent returns a plain Rack app, so it mounts directly in Rails routes (or any Rack host):
# config/routes.rb
Rails.application.routes.draw do
echo_agent = A2A.agent(agent_card: { "name" => "Echo Agent" }) do |env|
case env["a2a.operation"]
in "SendMessage"
A2A::Protocol::JsonSchema["Send Message Response"].new({})
end
end
mount echo_agent, at: "/echo"
end
See Rails & Rack Hosts for multiple agents, streaming, and testing over real HTTP.