Skip to content

Architecture

simple_a2a is organized into three top-level namespaces under the A2A module:

A2A
├── Models      — data classes (Task, Message, Part, Artifact, AgentCard, …)
├── Server      — HTTP server, routing, executor base, event fan-out
│   ├── App          (Roda JSON-RPC router)
│   ├── Base         (server bootstrap + Falcon runner)
│   ├── AgentExecutor (base class for your agent logic)
│   ├── Context      (per-request helper passed to executor)
│   ├── ResumeContext (Context + resume_message for interrupted tasks)
│   ├── EventRouter  (TypedBus SSE fan-out)
│   ├── PushSender   (webhook delivery with JWT signing)
│   └── FalconRunner (Falcon adapter)
├── Client      — async HTTP client
│   ├── Base    (JSON-RPC client over async/http)
│   └── SSE     (streaming subscribe client)
├── Storage     — task persistence
│   ├── Base    (abstract interface)
│   └── Memory  (in-process, thread-safe)
└── JsonRpc     — JSON-RPC 2.0 layer (request/response/error)

Request lifecycle

Client POST /
App (Roda)
    │  JSON-RPC parse + dispatch
handle_send
    │  creates Task (submitted)
    │  builds Context
Executor#call(ctx)          ← your code lives here
    │  ctx.task.start!
    │  ctx.emit_artifact(…)  → EventRouter → SSE subscribers
    │  ctx.task.complete!(…)
Storage#save(task)
JsonRpc::Response (task hash) → HTTP response

Key design decisions

Zeitwerk autoloading — all files under lib/simple_a2a/ load on demand. The top-level module is A2A (not SimpleA2a), achieved via a custom Zeitwerk inflector.

Async-first — the server runs inside Falcon's async reactor. The client uses Async::HTTP::Internet and wraps calls in Async { }.wait when invoked outside a reactor, keeping the public API synchronous.

One executor instanceServer::Base holds a single executor object. For concurrent requests, executors must be stateless (or use per-call state inside #call).

Pluggable storageStorage::Base defines the interface (save, find, list, delete). find! (raises on missing) is a convenience method on Storage::Memory only — not part of the required interface. Swap in Redis or PostgreSQL by subclassing and passing your implementation to Server::Base.

EventRouter — wraps TypedBus::MessageBus to provide per-task SSE channels. Channels are opened on first publish and closed when the SSE connection ends. subscribe transparently unwraps TypedBus::Delivery and calls ack!.