Skip to content
cuichangquanPublic

About

Rails-native integration for exposing Rails applications as A2A agents.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

a2a-rails



Rails-native integration for exposing Rails applications as A2A v1.0 agents.

Release status (2026-10-08): 0.2.0 is published as a stable release on RubyGems and GitHub. It includes the Rails/Zeitwerk and migration-generator fixes after rc2. The public Gem matches the verified artifact byte-for-byte; see the publication record.

Runnable Rails Demo / 実際に動くサンプル ⭐

a2a-rails-demo — independent Rails 8 Echo Agent

Run a complete standalone Rails Agent using the published RubyGems a2a-rails = 0.2.0 (not a Git source checkout). The separate Demo covers Agent Card discovery, JSON-RPC v1.0 SendMessage Task and direct Message, GetTask, ListTasks and error handling.

What is a2a-rails?

a2a-rails lets a Rails application expose an A2A-compatible Agent without making application code depend directly on SDK-specific request and response objects.

A2A Protocol
     ↓
Ruby A2A SDK
     ↓
a2a-rails
     ↓
Rails Application
     ↓
Business Logic

The Gem provides:

  • a Rails-native Agent / Skill DSL;
  • A2A Agent Card generation;
  • automatically mounted A2A HTTP endpoints;
  • synchronous Task execution by default, plus opt-in ActiveJob-backed async Task execution in published 0.2.0;
  • SDK-independent Handler inputs;
  • a process-local MemoryStore by default, plus an optional durable ActiveRecordStore in 0.2.0;
  • Rails generators for initial setup;
  • an internal Protocol Adapter boundary around the upstream SDK.

v0.1 is intentionally server-first and non-streaming.

Warning

Security / production use: v0.1.0 does not provide built-in authentication or per-caller Task authorization. Do not expose POST /a2a to untrusted clients. Before public deployment, protect the endpoint at your application's or network's security boundary. Security hardening is tracked in Issue #11.

Requirements

  • Ruby >= 3.3
  • Rails >= 8.0, < 8.2
  • A2A protocol version 1.0
  • agent2agent ~> 2.0.0

The original v0.1.0 baseline was verified against:

  • Ruby 3.3 / 3.4 / 4.0
  • Rails 8.0 / 8.1

Installation

Add the Gem to an existing Rails application:

bundle add a2a-rails

Then generate the initializer and an Agent scaffold:

bin/rails generate a2a:rails:install
bin/rails generate a2a:rails:agent echo

No explicit Engine mount or host config/routes.rb change is required.

Quick Start

The following Echo flow is verified against the published a2a-rails 0.1.0 Gem in a clean Rails 8.1 application.

1. Generate the setup

bin/rails generate a2a:rails:install
bin/rails generate a2a:rails:agent echo
mkdir -p app/services/echo

The install generator creates:

config/initializers/a2a_rails.rb

The Agent generator creates:

app/agents/echo_agent.rb

The generators deliberately do not create application business logic, jobs, migrations, Task Stores, or routing side effects.

2. Create the Handler

Create app/services/echo/reply.rb:

class Echo::Reply
  def self.call(message:, context:)
    text = message[:parts]
      .filter_map { |part| part[:text] }
      .join("\n")

    "Echo: #{text}"
  end
end

Handlers receive SDK-independent Ruby Hashes for message and context.

3. Define the Agent and Skill

Replace app/agents/echo_agent.rb with:

class EchoAgent < A2A::Rails::Agent
  name "Echo Agent"
  description "Echo messages"
  version "1.0"

  skill :reply,
    description: "Echo a message",
    tags: %w[echo],
    handler: Echo::Reply
end

A single Skill is selected automatically. No Router is required.

4. Register the Agent

Replace config/initializers/a2a_rails.rb with:

A2A::Rails.configure do |config|
  config.agent = "EchoAgent"
  config.public_base_url = ENV["A2A_PUBLIC_BASE_URL"]
end

For localhost, leave A2A_PUBLIC_BASE_URL unset or set it to http://localhost:3000.

The Agent class name remains a String until an A2A endpoint resolves it, preserving Rails autoload / reload behavior.

5. Check the Agent Card

Start Rails:

bin/rails server

Then request the Agent Card:

curl -sS http://localhost:3000/.well-known/agent-card.json \
  -H "A2A-Version: 1.0"

Expected essentials:

  • HTTP 200
  • Agent name Echo Agent
  • Skill ID reply
  • A2A interface URL ending in /a2a

6. Send a message

curl -sS -X POST http://localhost:3000/a2a \
  -H "Content-Type: application/json" \
  -H "A2A-Version: 1.0" \
  -d '{
    "jsonrpc": "2.0",
    "id": "1",
    "method": "SendMessage",
    "params": {
      "message": {
        "messageId": "msg-1",
        "role": "ROLE_USER",
        "parts": [{"text": "Hello"}]
      }
    }
  }'

A successful response reaches:

result.task.status.state == TASK_STATE_COMPLETED
Artifact Text Part == "Echo: Hello"

See docs/design/quick-start.md for the design and verification notes behind this flow.

Public Rails API

A minimal Agent looks like this:

class ShoppingAgent < A2A::Rails::Agent
  name "Shopping Agent"
  description "Search and purchase products"
  version "1.0"

  skill :search_products,
    description: "Search products",
    tags: %w[shopping search],
    handler: Shopping::SearchProducts
end

Handler:

class Shopping::SearchProducts
  def self.call(message:, context:)
    # Rails business logic
  end
end

Registration:

A2A::Rails.configure do |config|
  config.agent = "ShoppingAgent"
  config.public_base_url = ENV["A2A_PUBLIC_BASE_URL"]
end

v0.1 targets one public A2A Agent per Rails application.

For multiple Skills, the application supplies a Router. The Router receives skills: as a frozen Array<Symbol> of declared Skill IDs and may return a matching Symbol or String. Unknown selections raise A2A::Rails::UnknownSkillError; the Dispatcher does not silently choose the first Skill.

HTTP Endpoints

The Gem automatically exposes:

GET  /.well-known/agent-card.json
POST /a2a

The Rails Engine is mounted automatically.

Security in v0.2.0 (not in v0.1.0)

Published 0.2.0 includes a host-provided config.authenticate_request callback for POST /a2a, plus per-principal Task ownership checks. Without an authenticator, production and other non-development/test environments fail closed; the local development/test Quick Start remains available. See Authentication guide.

Important: These protections are absent from RubyGems 0.1.0 but included in the published 0.2.0.rc2 pre-release; stable 0.2.0 includes additional fixes. Neither pre-release nor stable source automatically makes a deployed endpoint production-secure. Distributed rate limits, business authorization and the deployment review remain tracked in Issue #11.

Step 16-4 request hardening (included in v0.2.0)

Published 0.2.0 includes a bounded JSON-RPC body (config.max_request_bytes, default 1 MiB), Content-Type validation, stricter parameter checks, a pagination snapshot cap, and reduced exception logging. See Request Hardening Guide.

Rate limiting, application-specific authorization and production deployment safeguards remain the host's responsibility or future work. Step 16-5 adds explicit A2A Agent Card Bearer authentication advertisement in published 0.2.0; it must match the host verifier. These improvements are not included in the published v0.1.0 Gem; see the published stable 0.2.0 release.

Production deployment review (Step 16-6)

Current verdict: NO-GO by default for open public production. The default MemoryStore remains process-local. Step 21 provides an optional durable ActiveRecordStore, and Step 22 provides optional ActiveJob Task execution, but production async operation requires both a shared/durable Task Store and a durable queue backend operated by the host. A real deployment must also provide credential verification, business authorization, TLS/proxy restrictions, distributed rate limits, execution budgets, observability and deployment-specific verification.

Do not interpret successful source-tree CI or a merge to main as publication of a new RubyGems release.

Agent Card

Agent Cards are generated from the Agent / Skill DSL.

Behavior in v0.1:

  • Skill IDs and default display names come from Skill declarations;
  • optional examples, input_modes, and output_modes map to A2A Agent Card fields;
  • Handler internals are not exposed;
  • public_base_url wins when configured, otherwise the request base URL is used;
  • default input/output mode is text/plain;
  • Streaming, Push Notifications, and Extended Agent Cards are disabled;
  • generated Cards pass the agent2agent 2.0.0 Agent Card schema.

Task Lifecycle

Internally, a2a-rails keeps SDK-independent Task states:

SUBMITTED
    ↓
WORKING
    ├──→ COMPLETED
    ├──→ FAILED
    └──→ REJECTED

Cancellation can move a non-terminal Task to CANCELED.

Handler result mapping:

normal return                  → COMPLETED
A2A::Rails::RejectedTask       → REJECTED
unexpected exception           → FAILED

Artifact mapping:

String       → Text Part
Hash / Array → Data Part
nil          → no Artifact
other object → ArtifactMappingError

Included since v0.2.0 (not in v0.1.0):
A2A::Rails::FileArtifact.bytes(...) → File Part with base64 raw bytes
A2A::Rails::FileArtifact.url(...)   → File Part with an HTTPS URL

File Artifact output is included in v0.2.0. Rails Handlers can explicitly return a FileArtifact.bytes(data:, filename:, media_type:) or FileArtifact.url(url:, filename:, media_type:). See the File Artifact output guide. Input file Parts, downloading remote URLs and production file authorization are not supplied by the Gem.

Included in v0.2.0 (Step 17-4): A2A v1.0 also permits a direct Message from SendMessage. The host Agent can opt into response_mode :message or select the mode using a callable; the default remains :task. Direct replies do not create Task records. See the Direct Message response guide for the contract and safety implications.

Supported Task operations:

  • SendMessage
  • GetTask
  • ListTasks
  • CancelTask

ListTasks supports context/state/timestamp filters, stable newest-first ordering, page sizes 1–100, and opaque snapshot pagination.

The default Task::MemoryStore is thread-safe but process-local. Tasks and pagination cursors are not durable across process restarts and are not shared between processes.

Included in v0.2.0 (Step 21): applications that need durable, multi-worker Task state can opt into config.task_store = :active_record. The ActiveRecordStore uses owner-scoped SQL access, row-locked transitions, signed keyset cursors, terminal retention, bounded pruning and maintenance limits. See ActiveRecord Task Store. PostgreSQL 16 persistence/locking smoke is verified in CI.

Included in v0.2.0 (Step 22): Task execution remains synchronous by default. Hosts can opt into ActiveJob-backed async execution globally, per Agent, or per Skill; precedence is Skill > Agent > global.

A2A::Rails.configure do |config|
  config.task_store = :active_record
  config.task_execution_mode = :async
end

class ReportsAgent < A2A::Rails::Agent
  execution_mode :sync

  skill :build_report,
    description: "Build a report",
    tags: %w[report],
    handler: Reports::Build,
    execution_mode: :async
end

Async SendMessage persists and returns a SUBMITTED Task, then a Gem-owned ActiveJob worker claims it atomically and executes the already-selected Skill. Direct-Message responses remain synchronous because they do not create a persisted Task to poll.

Production async use requires a shared/durable Task Store plus a durable ActiveJob backend. The Gem does not provide a distributed transaction between Task persistence and queue enqueue: a small crash window remains after Task commit and before queue acknowledgement. Generic Handler retries are intentionally disabled; duplicate Job delivery is suppressed at Task claim, but exactly-once external side effects are not guaranteed. Running cancellation is logical/best-effort, and ambiguous WORKING Tasks are not automatically replayed after a worker crash. See ActiveJob Task Execution and queue adapter / HTTP async verification.

Cancellation changes Task state atomically, but does not stop already-running Handler code or reverse application side effects.

Architecture

Key boundaries:

  • Rails Engine / controllers / routes form the Rails integration layer;
  • SDK-specific behavior stays behind Protocol::Adapter / Protocol::Agent2AgentAdapter;
  • A2A camelCase fields, TASK_STATE_*, SDK schema objects, and SDK errors stay in the Protocol layer;
  • Agent / Handler application constants are resolved lazily through Rails;
  • ActiveRecord is optional and loaded only when ActiveRecordStore is selected; ActiveJob is not a runtime requirement.

Gem structure:

lib/a2a/rails/
├── agent.rb
├── skill.rb
├── dispatcher.rb
├── configuration.rb
├── runtime.rb
├── engine.rb
├── agent_card/
├── task/
└── protocol/

lib/generators/a2a/rails/
├── install_generator.rb
├── agent_generator.rb
└── templates/

v0.1 Scope

Included:

  • Rails integration
  • A2A::Rails::Agent
  • Skill DSL and Handler dispatch
  • Agent Card generation
  • /.well-known/agent-card.json
  • POST /a2a
  • synchronous Task lifecycle
  • SendMessage, GetTask, ListTasks, CancelTask
  • A2A-Version: 1.0 validation
  • in-memory Task Store
  • Rails Engine / Routes
  • Configuration
  • install / agent generators
  • Rails logging boundary

Not included in v0.1:

  • A2A Client
  • ActiveRecord Task Store
  • ActiveJob Task execution
  • SSE / BiDi Streaming
  • Push Notifications
  • gRPC
  • Human-in-the-loop flows
  • INPUT_REQUIRED / AUTH_REQUIRED
  • OAuth Server
  • Agent Registry / Marketplace
  • Authorization Engine
  • ActingFor integration
  • Admin UI
  • LLM Agent Framework
  • Orchestration Framework

Verification

The v0.1.0 release candidate completed:

13 / 13 CI jobs green
70 tests
240 assertions
0 failures
0 errors
0 skips

After publication, a2a-rails 0.1.0 was fetched back from RubyGems and its SHA256 matched the exact artifact that was pushed.

Final published artifact SHA256:

23d34bde6f436723bf01735f3a507cf8529975b1dfed5d5da9b063fc0480b19f

A fresh Rails 8.1 application then installed the published Gem from RubyGems and verified:

Agent Card HTTP: 200
Agent name: Echo Agent
Skill: reply
SendMessage HTTP: 200
Task state: TASK_STATE_COMPLETED
Artifact: Echo: Hello

See docs/release/v0.1.0-record.md for the complete release evidence.

Cross-language A2A interoperability — source verification

Step 18 / PR #27 verified the official Python a2a-sdk==1.2.2 and Go a2a-go/v2 v2.6.0 clients against a loopback-only Rails JSON-RPC A2A v1.0 Agent. Both discovered Agent Cards, decoded Task and direct Message responses, queried Tasks and checked terminal cancellation and version errors.

This evidence applies to the later source/pre-release line, not RubyGems v0.1.0; these tests do not certify full A2A interoperability or public-production safety.

Test Strategy

v0.1 uses Minitest with four layers:

4. Protocol E2E / Smoke Tests
3. Rails Integration Tests
2. Adapter Contract Tests
1. Core Unit Tests

The CI matrix covers:

  • Gem: Ruby 3.3 / 3.4 / 4.0
  • SDK spike: Ruby 3.3 / 3.4 / 4.0
  • Rails: Ruby 3.3 / 3.4 / 4.0 × Rails 8.0 / 8.1
  • Packaged Gem: Ruby 3.4 + clean Rails 8.1 application

Known warning-enabled output from upstream agent2agent 2.0.0 can include circular-require, indentation, and URI-parser warnings. Those warnings are distinct from the Rack-environment INFO logging that a2a-rails suppresses at the Rails-facing adapter boundary.

Release Documents

Design Documents

License

The Gem is available as open source under the terms of the MIT License. See LICENSE.

Official A2A Resources / A2A公式資料

Articles & Community / 紹介記事・コミュニティ

Japanese articles / 日本語の紹介記事

Community submissions / コミュニティへの投稿

About

Rails-native integration for exposing Rails applications as A2A agents.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages