simple_a2a 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -0
- data/README.md +78 -38
- data/compare_agent2agent.md +460 -0
- data/docs/api/client/index.md +19 -0
- data/docs/api/index.md +4 -3
- data/docs/api/models/index.md +13 -11
- data/docs/api/server/index.md +42 -10
- data/docs/api/storage/index.md +0 -1
- data/docs/architecture/index.md +17 -15
- data/docs/architecture/protocol.md +16 -1
- data/docs/assets/images/simple_a2a.jpg +0 -0
- data/docs/examples/agent-chaining.md +107 -0
- data/docs/examples/auth-headers.md +105 -0
- data/docs/examples/cancellation.md +105 -0
- data/docs/examples/index.md +123 -52
- data/docs/examples/interrupted-states.md +114 -0
- data/docs/examples/multipart.md +103 -0
- data/docs/examples/push-notifications.md +117 -0
- data/docs/examples/resubscribe.md +129 -0
- data/docs/examples/sqlite-storage.md +131 -0
- data/docs/examples/streaming.md +1 -4
- data/docs/guides/push-notifications.md +4 -1
- data/docs/guides/streaming.md +34 -5
- data/docs/index.md +55 -27
- data/examples/04_resubscribe/client.rb +140 -0
- data/examples/04_resubscribe/server.rb +75 -0
- data/examples/05_cancellation/client.rb +150 -0
- data/examples/05_cancellation/server.rb +77 -0
- data/examples/06_push_notifications/client.rb +192 -0
- data/examples/06_push_notifications/server.rb +123 -0
- data/examples/07_agent_chaining/client.rb +120 -0
- data/examples/07_agent_chaining/server.rb +150 -0
- data/examples/08_interrupted_states/client.rb +148 -0
- data/examples/08_interrupted_states/server.rb +142 -0
- data/examples/09_multipart/client.rb +117 -0
- data/examples/09_multipart/server.rb +97 -0
- data/examples/10_auth_headers/client.rb +92 -0
- data/examples/10_auth_headers/server.rb +98 -0
- data/examples/11_sqlite_storage/Brewfile +1 -0
- data/examples/11_sqlite_storage/Gemfile +9 -0
- data/examples/11_sqlite_storage/client.rb +114 -0
- data/examples/11_sqlite_storage/run +154 -0
- data/examples/11_sqlite_storage/server.rb +131 -0
- data/examples/README.md +384 -0
- data/lib/simple_a2a/client/sse.rb +15 -0
- data/lib/simple_a2a/server/app.rb +131 -45
- data/lib/simple_a2a/server/base.rb +19 -17
- data/lib/simple_a2a/server/broadcast_registry.rb +24 -0
- data/lib/simple_a2a/server/multi_agent.rb +1 -1
- data/lib/simple_a2a/server/push_config_store.rb +29 -0
- data/lib/simple_a2a/server/push_sender.rb +1 -0
- data/lib/simple_a2a/server/task_broadcast.rb +46 -0
- data/lib/simple_a2a/version.rb +1 -1
- metadata +38 -20
- data/lib/simple_a2a/server/event_router.rb +0 -50
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
# Compare & Contrast: `agent2agent` vs `simple_a2a`
|
|
2
|
+
|
|
3
|
+
**`agent2agent`** — https://github.com/general-intelligence-systems/agent2agent
|
|
4
|
+
**`simple_a2a`** — https://github.com/MadBomber/simple_a2a
|
|
5
|
+
**Reviewed:** 2026-05-08
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Philosophy and Scope
|
|
10
|
+
|
|
11
|
+
| | `agent2agent` | `simple_a2a` |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| **Philosophy** | Spec-first: derive everything from the canonical `.proto` and `.json` schema files | Pragmatic: hand-craft a clean, minimal Ruby interface that covers real use cases |
|
|
14
|
+
| **Complexity** | Heavy — brings in protobuf parsing, JSON Schema validation, SQLite, structured logging | Lightweight — each component is a short, readable Ruby class |
|
|
15
|
+
| **Target user** | Teams that need the full A2A spec surface, including multi-tenant and OpenTelemetry | Individual developers or small teams who want a drop-in, understandable library |
|
|
16
|
+
| **License** | Apache 2.0 | MIT |
|
|
17
|
+
| **Ruby requirement** | >= 3.2 | >= 3.2 |
|
|
18
|
+
| **Namespace** | `A2A` | `A2A` (same) |
|
|
19
|
+
| **Gem name** | `agent2agent` | `simple_a2a` |
|
|
20
|
+
| **Authorship** | "A2A Contributors" (organizational) | Dewayne VanHoozer (individual) |
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 2. Protocol Coverage
|
|
25
|
+
|
|
26
|
+
### Operations Implemented
|
|
27
|
+
|
|
28
|
+
| Operation | `agent2agent` | `simple_a2a` |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| SendMessage / `tasks/send` | Yes | Yes |
|
|
31
|
+
| SendStreamingMessage / `tasks/sendSubscribe` | Yes | Yes |
|
|
32
|
+
| GetTask / `tasks/get` | Yes | Yes |
|
|
33
|
+
| ListTasks / `tasks/list` | Yes | Yes |
|
|
34
|
+
| CancelTask / `tasks/cancel` | Yes | Yes |
|
|
35
|
+
| SubscribeToTask (SSE poll of existing task) | Yes | No |
|
|
36
|
+
| CreateTaskPushNotificationConfig | Yes (full) | Stub only |
|
|
37
|
+
| GetTaskPushNotificationConfig | Yes (full) | Stub only |
|
|
38
|
+
| ListTaskPushNotificationConfigs | Yes (full) | Stub only |
|
|
39
|
+
| DeleteTaskPushNotificationConfig | Yes (full) | Stub only |
|
|
40
|
+
| GetExtendedAgentCard | Yes | No |
|
|
41
|
+
|
|
42
|
+
`agent2agent` implements all 11 A2A protocol operations. `simple_a2a` implements 5 core operations plus push notification stubs.
|
|
43
|
+
|
|
44
|
+
### Transport Bindings
|
|
45
|
+
|
|
46
|
+
| | `agent2agent` | `simple_a2a` |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| **JSON-RPC 2.0** | Yes — `POST /a2a` | Yes — `POST /` |
|
|
49
|
+
| **HTTP+JSON REST** | Yes — `POST /message:send`, `GET /tasks/{id}`, etc. | No |
|
|
50
|
+
| **Agent Discovery** | `GET /.well-known/agent-card.json` (spec-compliant path) | `GET /agentCard` (non-standard path) |
|
|
51
|
+
|
|
52
|
+
`agent2agent` supports both the JSON-RPC and REST wire formats from the spec. `simple_a2a` is JSON-RPC only. The agent card path difference means `simple_a2a` is not spec-compliant for discovery.
|
|
53
|
+
|
|
54
|
+
### Protocol Versioning
|
|
55
|
+
|
|
56
|
+
`simple_a2a` checks the `A2A-Version` request header and rejects unsupported versions (`SUPPORTED_VERSIONS = %w[1.0 0.3]`) with a JSON-RPC error response. `agent2agent` does not implement version negotiation.
|
|
57
|
+
|
|
58
|
+
### Multi-Tenant Paths
|
|
59
|
+
|
|
60
|
+
`agent2agent` supports tenant-prefixed route variants (`/{tenant}/message:send`, etc.) built into its REST binding. `simple_a2a` has no multi-tenant concept, but its `MultiAgent` class achieves path-based isolation via `Rack::URLMap`.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 3. Server Architecture
|
|
65
|
+
|
|
66
|
+
### `agent2agent` — Rack Middleware Chain
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
Rack::URLMap
|
|
70
|
+
/.well-known/agent-card.json → AgentCardHandler
|
|
71
|
+
/a2a → Bindings::JsonRpc → Server::Env → Server::Triage → Server::Dispatcher
|
|
72
|
+
/ → Bindings::Rest → Server::Env → Server::Triage → Server::Dispatcher
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- Each operation has its own handler class in `lib/a2a/server/` (11 handler files)
|
|
76
|
+
- `Triage` resolves the target operation from either the JSON-RPC method name or the REST verb+path — derived from `data/a2a.proto` at load time, not hard-coded
|
|
77
|
+
- `Dispatcher` routes `env["a2a.operation"]` to registered handler objects via duck-typing
|
|
78
|
+
|
|
79
|
+
### `simple_a2a` — Roda App
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
A2A::Server::App < Roda
|
|
83
|
+
GET /agentCard → return agent_card.to_h
|
|
84
|
+
POST / → parse JSON-RPC, dispatch by method name (case statement)
|
|
85
|
+
tasks/sendSubscribe → handle_send_subscribe (SSE)
|
|
86
|
+
tasks/send → handle_send
|
|
87
|
+
tasks/get → handle_get
|
|
88
|
+
tasks/list → handle_list
|
|
89
|
+
tasks/cancel → handle_cancel
|
|
90
|
+
tasks/pushNotification/* → handle_push_* (stubs)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- Uses Roda plugins: `json`, `json_parser`, `halt`, `all_verbs`
|
|
94
|
+
- Dispatch is a `case rpc_req.method` in `App#dispatch` — explicit and readable but requires a code change for each new operation
|
|
95
|
+
- `Server::Base` is the public entry point; it creates a fresh anonymous `App` subclass per instance so class-level state doesn't bleed between servers
|
|
96
|
+
|
|
97
|
+
**Key difference:** `agent2agent` is extensible via proto-driven operation registration; `simple_a2a` is explicit but static. Adding a new operation in `simple_a2a` means editing `dispatch`; in `agent2agent`, it follows automatically from the proto file.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 4. Agent / Executor Model
|
|
102
|
+
|
|
103
|
+
### `agent2agent` — DSL with `on` blocks
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
agent = A2A::Agent.new do
|
|
107
|
+
on "SendMessage", "SendStreamingMessage" do |context|
|
|
108
|
+
task = context.store.create(context.request)
|
|
109
|
+
stream = context.stream
|
|
110
|
+
Async do
|
|
111
|
+
result = robot.run(context.request.params[:message])
|
|
112
|
+
context.store.complete(task.id, result)
|
|
113
|
+
stream.event(result, type: "result")
|
|
114
|
+
stream.finish
|
|
115
|
+
end
|
|
116
|
+
context.respond(task)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
server = A2A::Server.new
|
|
120
|
+
server.register(agent)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The `Agent` DSL maps operation names to blocks. Multiple operations can share one handler. The `Context` object exposes `respond(result)`, `stream`, `store`, `agent_card`, and `request`. The `respond` call returns a synchronous response; SSE is opt-in via `context.stream`.
|
|
124
|
+
|
|
125
|
+
### `simple_a2a` — Abstract class with `#call` / `#cancel`
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
class MyExecutor < A2A::Server::AgentExecutor
|
|
129
|
+
def call(context)
|
|
130
|
+
context.task.working!
|
|
131
|
+
context.emit_status
|
|
132
|
+
result = do_work(context.message)
|
|
133
|
+
context.task.complete!(result)
|
|
134
|
+
context.emit_status(final: true)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
server = A2A::Server::Base.new(
|
|
139
|
+
agent_card: card,
|
|
140
|
+
executor: MyExecutor.new,
|
|
141
|
+
storage: A2A::Storage::Memory.new
|
|
142
|
+
)
|
|
143
|
+
server.run
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`AgentExecutor` is an abstract class with `#call(context)` and a default `#cancel(context)` that calls `task.cancel!`. The executor is responsible for advancing task state explicitly. The `Context` object exposes `task`, `message`, `storage`, `event_router`, `save_task`, `emit_status`, and `emit_artifact`.
|
|
147
|
+
|
|
148
|
+
**Key difference:** `agent2agent`'s DSL is more concise for simple cases but less obvious for control flow. `simple_a2a`'s abstract class pattern is more explicit about what the implementer must do and is easier to test in isolation.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 5. Context Object
|
|
153
|
+
|
|
154
|
+
| Method / Attribute | `agent2agent` | `simple_a2a` |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| Access the task | via `context.store` | `context.task` (direct reference) |
|
|
157
|
+
| Send a response | `context.respond(result)` | return value from `#call` (App handles it) |
|
|
158
|
+
| SSE stream | `context.stream` | SSE handled transparently by App; executor yields events via `emit_status` / `emit_artifact` |
|
|
159
|
+
| Task store | `context.store` | `context.storage` |
|
|
160
|
+
| Agent card | `context.agent_card` | not on context — on `App` class |
|
|
161
|
+
| Inbound message | `context.request` | `context.message` |
|
|
162
|
+
| Persist task | via `store.complete(...)` | `context.save_task` or `context.emit_status` (auto-saves) |
|
|
163
|
+
| Emit status event | N/A (store methods do it) | `context.emit_status(final: false)` |
|
|
164
|
+
| Emit artifact event | N/A | `context.emit_artifact(artifact, append:, last_chunk:)` |
|
|
165
|
+
|
|
166
|
+
`simple_a2a`'s context makes task-state advancement and event emission first-class operations. `agent2agent`'s context is thinner on the executor side — more work goes through the store.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 6. Schema and Models
|
|
171
|
+
|
|
172
|
+
### `agent2agent` — Spec-Driven Dynamic Classes
|
|
173
|
+
|
|
174
|
+
- Loads `data/a2a.json` (47-type JSON Schema bundle, 73 KB) at startup
|
|
175
|
+
- Dynamically generates one `Definition` subclass per schema type
|
|
176
|
+
- Each class provides: snake_case readers, `to_h` (camelCase output), `valid?`/`valid!` (full JSON Schema validation via `json_schemer`), and `==`
|
|
177
|
+
- Also parses `data/a2a.proto` (34 KB) to derive operation metadata, HTTP bindings, and streaming flags
|
|
178
|
+
- **Validation:** full JSON Schema compliance against the official A2A spec
|
|
179
|
+
|
|
180
|
+
### `simple_a2a` — Hand-Written `Models::Base` DSL
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
class Message < Base
|
|
184
|
+
attribute :role, type: String, required: true
|
|
185
|
+
attribute :parts, type: [Part]
|
|
186
|
+
attribute :metadata
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- ~15 hand-written model classes in `lib/simple_a2a/models/`
|
|
191
|
+
- `Models::Base` provides: `attribute` macro, `from_hash` (with camelCase/snake_case coercion), `to_h`, `to_json`, `valid?` (required-field check only), `==`
|
|
192
|
+
- Type coercion handles nested models and arrays of models
|
|
193
|
+
- **Validation:** only checks `required:` fields; no JSON Schema enforcement
|
|
194
|
+
|
|
195
|
+
**Key difference:** `agent2agent` stays in sync with the official spec automatically (update the JSON/proto files, get new model classes). `simple_a2a` requires manual updates when the spec changes but is far easier to read and debug. `simple_a2a`'s validation is deliberately minimal — it trusts that callers pass valid data.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 7. SSE Streaming
|
|
200
|
+
|
|
201
|
+
Both use `Protocol::HTTP::Body::Writable` as the SSE body — the right approach for Falcon compatibility.
|
|
202
|
+
|
|
203
|
+
### `agent2agent`
|
|
204
|
+
|
|
205
|
+
- `A2A::SSE::Stream < Protocol::HTTP::Body::Writable`
|
|
206
|
+
- Two subclasses: `JsonRpcStream` (wraps events in JSON-RPC envelope) and `RestStream` (plain event format)
|
|
207
|
+
- `stream.event(hash, type: "result")` writes formatted SSE chunks
|
|
208
|
+
- `stream.finish` calls `close_write`
|
|
209
|
+
- The executor has direct access to the stream via `context.stream`
|
|
210
|
+
|
|
211
|
+
### `simple_a2a`
|
|
212
|
+
|
|
213
|
+
- Uses `Protocol::HTTP::Body::Writable` directly, not subclassed
|
|
214
|
+
- In `handle_send_subscribe`, an anonymous object (duck-typed router) is created inline that writes SSE-formatted JSON-RPC events to the writable body
|
|
215
|
+
- The executor runs in a sibling `Async::Task`; it calls `emit_status` / `emit_artifact` which publish to the anonymous router, which writes to the writable body
|
|
216
|
+
- `output.close_write` is called in the `ensure` block
|
|
217
|
+
|
|
218
|
+
**Key difference:** `agent2agent`'s stream is a first-class object the executor interacts with directly. `simple_a2a`'s SSE wiring is internal to `App` — the executor emits domain events and the App translates them to SSE. The `simple_a2a` approach is cleaner for the executor author but the anonymous-object pattern in `handle_send_subscribe` is the trickiest code in the library.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 8. Task Storage
|
|
223
|
+
|
|
224
|
+
### `agent2agent`
|
|
225
|
+
|
|
226
|
+
Two implementations:
|
|
227
|
+
- `A2A::TaskStore` — in-memory, mutex-synchronized
|
|
228
|
+
- `A2A::Sqlite` (~16 KB) — WAL mode, indexed, production-ready, fiber-safe via `Async::Queue` pub/sub
|
|
229
|
+
|
|
230
|
+
Both include integrated pub/sub (`Store::PubSub`) and webhook delivery (`Store::Webhooks`). Pub/sub uses `Async::Queue` per task subscriber — fiber-safe without locks.
|
|
231
|
+
|
|
232
|
+
### `simple_a2a`
|
|
233
|
+
|
|
234
|
+
One implementation:
|
|
235
|
+
- `Storage::Memory` — in-memory, mutex-synchronized (`@mutex = Mutex.new`)
|
|
236
|
+
- `Storage::Base` — abstract base class defines the interface: `save`, `find`, `find!`, `delete`, `list`, `size`, `clear`
|
|
237
|
+
|
|
238
|
+
No persistent storage. Pub/sub is separated from storage into `EventRouter`.
|
|
239
|
+
|
|
240
|
+
**Key difference:** `agent2agent` ships production storage out of the box. `simple_a2a` provides the extension interface but the implementer must write their own persistent store. `agent2agent`'s storage and pub/sub are tightly coupled; `simple_a2a` separates them.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 9. Pub/Sub and Event Routing
|
|
245
|
+
|
|
246
|
+
### `agent2agent` — `Store::PubSub` with `Async::Queue`
|
|
247
|
+
|
|
248
|
+
- Each task gets an `Async::Queue` per subscriber
|
|
249
|
+
- `nil` sentinel signals end-of-stream
|
|
250
|
+
- Fully fiber-safe — no locks needed under Falcon's fiber scheduler
|
|
251
|
+
- Integrated into the task store; the `SubscribeToTask` operation connects clients to these queues
|
|
252
|
+
|
|
253
|
+
### `simple_a2a` — `EventRouter` wrapping `TypedBus`
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
@bus = TypedBus::MessageBus.new
|
|
257
|
+
@bus.add_channel(task_id.to_sym, type: nil, timeout: nil)
|
|
258
|
+
@bus.publish(sym, event)
|
|
259
|
+
@bus.subscribe(sym) { |delivery| block.call(delivery.message); delivery.ack! }
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
- Channels are keyed by task ID (as Symbol)
|
|
263
|
+
- Typed, ack-based message delivery via `typed_bus` gem
|
|
264
|
+
- `EventRouter` is dependency-injected — in SSE mode, `App` swaps in an anonymous router that writes directly to the `Writable` body
|
|
265
|
+
|
|
266
|
+
**Key difference:** `agent2agent`'s `Async::Queue` is fiber-native and simpler internally. `simple_a2a`'s `TypedBus` adds ack semantics and typing but introduces an external dependency. The swap-in anonymous router for SSE is a clever workaround for the fact that `TypedBus` isn't directly wired to the HTTP body.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 10. Client
|
|
271
|
+
|
|
272
|
+
### `agent2agent` — Dynamically Generated Methods
|
|
273
|
+
|
|
274
|
+
- Methods generated from `Proto.operations` mapped to snake_case
|
|
275
|
+
- Uses `Async::HTTP::Internet` for non-blocking HTTP
|
|
276
|
+
- JSON-RPC 2.0 requests to `/a2a`
|
|
277
|
+
- Auto-incrementing request IDs
|
|
278
|
+
- Also fetches `/.well-known/agent-card.json`
|
|
279
|
+
|
|
280
|
+
### `simple_a2a` — Hand-Written Named Methods
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
client.agent_card
|
|
284
|
+
client.send_task(message: msg)
|
|
285
|
+
client.get_task(task_id)
|
|
286
|
+
client.list_tasks
|
|
287
|
+
client.cancel_task(task_id)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
- `Async::HTTP::Internet` — same concurrency model
|
|
291
|
+
- Context-aware: works inside an existing `Async::Task` or creates its own
|
|
292
|
+
- Methods return typed `Models::*` objects
|
|
293
|
+
- Also includes `Client::SSE` for consuming streaming responses
|
|
294
|
+
|
|
295
|
+
**Key difference:** `agent2agent`'s dynamic generation means new operations are available automatically when the proto file is updated. `simple_a2a`'s hand-written methods are easier to introspect, document, and test, but require manual additions for new operations. `simple_a2a`'s async context awareness (using existing task if present, creating one if not) is a practical improvement.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 11. Push Notifications
|
|
300
|
+
|
|
301
|
+
### `agent2agent`
|
|
302
|
+
|
|
303
|
+
- Full async webhook delivery via `Store::Webhooks` + `Async::HTTP`
|
|
304
|
+
- Per-task push notification config CRUD with full protocol storage
|
|
305
|
+
- Webhook auth: `Authorization: Bearer <credentials>` + optional `X-A2A-Notification-Token`
|
|
306
|
+
- Runs delivery in background fibers
|
|
307
|
+
|
|
308
|
+
### `simple_a2a`
|
|
309
|
+
|
|
310
|
+
- `PushSender` class with real HTTP delivery via `Net::HTTP`
|
|
311
|
+
- JWT signing (RS256) with payload hash, configurable private key + key ID + issuer
|
|
312
|
+
- Supports `bearer` (JWT), `token` (static), or custom header auth schemes
|
|
313
|
+
- 5/10 second open/read timeout
|
|
314
|
+
- Push notification config CRUD operations are stubs — they respond successfully but do not store configs
|
|
315
|
+
|
|
316
|
+
**Key difference:** `agent2agent` has full push notification infrastructure. `simple_a2a` has a capable delivery mechanism (`PushSender`) but the server-side config storage is not implemented — the stubs accept requests without persisting anything. A real push notification flow requires the implementer to wire `PushSender` into the executor manually.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## 12. Multi-Agent Support
|
|
321
|
+
|
|
322
|
+
### `agent2agent`
|
|
323
|
+
|
|
324
|
+
```ruby
|
|
325
|
+
server = A2A::Server.new
|
|
326
|
+
server.register(agent1)
|
|
327
|
+
server.register(agent2)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Multiple agents registered on a single server; the dispatcher routes to the correct agent based on its declared operations.
|
|
331
|
+
|
|
332
|
+
### `simple_a2a`
|
|
333
|
+
|
|
334
|
+
```ruby
|
|
335
|
+
A2A::Server::MultiAgent.new(
|
|
336
|
+
agents: {
|
|
337
|
+
"/anthropic" => { agent_card: card1, executor: exec1 },
|
|
338
|
+
"/openai" => { agent_card: card2, executor: exec2 }
|
|
339
|
+
},
|
|
340
|
+
port: 9292
|
|
341
|
+
).run
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
`MultiAgent` uses `Rack::URLMap` — each agent gets its own URL path prefix. Each agent gets a fresh anonymous `App` subclass so class-level config state doesn't bleed.
|
|
345
|
+
|
|
346
|
+
**Key difference:** `agent2agent` shares a single endpoint; agents are distinguished by operation. `simple_a2a` isolates agents at the URL level — different agents at different paths. The `Rack::URLMap` approach is architecturally cleaner for true multi-agent isolation.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## 13. Dependencies
|
|
351
|
+
|
|
352
|
+
### Runtime comparison
|
|
353
|
+
|
|
354
|
+
| Gem | `agent2agent` | `simple_a2a` |
|
|
355
|
+
|---|---|---|
|
|
356
|
+
| `async` | Yes | Yes |
|
|
357
|
+
| `async-http` | Yes | Yes |
|
|
358
|
+
| `rack` | Yes | Yes |
|
|
359
|
+
| `protocol-http` | Yes (explicit) | Yes (via falcon) |
|
|
360
|
+
| `falcon` | Dev only | Yes (runtime) |
|
|
361
|
+
| `roda` | No | Yes |
|
|
362
|
+
| `google-protobuf` | Yes | No |
|
|
363
|
+
| `json_schemer` | Yes | No |
|
|
364
|
+
| `sqlite3` | Yes | No |
|
|
365
|
+
| `console` | Yes | No |
|
|
366
|
+
| `scampi` | Yes (inline tests) | No |
|
|
367
|
+
| `jwt` | No | Yes |
|
|
368
|
+
| `simple_flow` | No | Yes |
|
|
369
|
+
| `typed_bus` | No | Yes |
|
|
370
|
+
| `zeitwerk` | No | Yes |
|
|
371
|
+
| `logger` | No | Yes |
|
|
372
|
+
|
|
373
|
+
`agent2agent` pulls in protobuf, JSON Schema validation, and SQLite. `simple_a2a` pulls in Roda, JWT, and two smaller gems (`typed_bus`, `simple_flow`). `agent2agent` is heavier in binary dependencies (native extensions: `google-protobuf`, `sqlite3`). `simple_a2a`'s heavier non-obvious dependency is `typed_bus`, which is a less-established gem.
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## 14. Observability and Tracing
|
|
378
|
+
|
|
379
|
+
| | `agent2agent` | `simple_a2a` |
|
|
380
|
+
|---|---|---|
|
|
381
|
+
| **Structured logging** | `console` gem throughout | `A2A.logger` (stdlib Logger); warn-level in PushSender |
|
|
382
|
+
| **Distributed tracing** | `lib/traces/provider/a2a/` — OpenTelemetry-compatible spans for bindings and dispatcher | None |
|
|
383
|
+
| **Operation counters** | `Store::Processor` tracks call/complete/failed counts | None |
|
|
384
|
+
|
|
385
|
+
`agent2agent` is observability-ready out of the box for teams running distributed agent systems. `simple_a2a` has minimal logging.
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## 15. Testing Strategy
|
|
390
|
+
|
|
391
|
+
### `agent2agent` — Inline Tests via `scampi`
|
|
392
|
+
|
|
393
|
+
- Tests live inside source files, co-located with the code they test
|
|
394
|
+
- `test do ... end` blocks with `.should`-style assertions
|
|
395
|
+
- No separate `test/` directory
|
|
396
|
+
|
|
397
|
+
### `simple_a2a` — Minitest in `test/`
|
|
398
|
+
|
|
399
|
+
- Separate `test/` directory with conventional Minitest structure
|
|
400
|
+
- 98% code coverage measured via `simplecov`
|
|
401
|
+
- `rack-test` for HTTP-level integration tests
|
|
402
|
+
- Separate files per module: `test/server/test_app.rb`, `test/server/test_app_sse.rb`, `test/integration/test_round_trip.rb`, etc.
|
|
403
|
+
|
|
404
|
+
**Key difference:** `scampi` inline tests reduce context-switching but non-standard tooling makes CI integration harder. Minitest is familiar, with rich ecosystem support. `simple_a2a`'s 98% coverage is a strong signal of test completeness.
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## 16. Examples
|
|
409
|
+
|
|
410
|
+
| | `agent2agent` | `simple_a2a` |
|
|
411
|
+
|---|---|---|
|
|
412
|
+
| Count | 6 | 3 |
|
|
413
|
+
| Basic usage | Yes | Yes (`01_basic_usage/`) |
|
|
414
|
+
| Streaming | Yes | Yes (`02_streaming/`) |
|
|
415
|
+
| LLM integration | No | Yes (`03_llm_research/`) |
|
|
416
|
+
| Async/background jobs | Yes | No |
|
|
417
|
+
| Multi-turn conversation | Yes | No |
|
|
418
|
+
| Multi-agent | Yes | No (but `MultiAgent` class exists) |
|
|
419
|
+
| Push notifications | Yes | No |
|
|
420
|
+
| Docker/Compose | Yes | No |
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## 17. Summary Assessment
|
|
425
|
+
|
|
426
|
+
### Where `agent2agent` wins
|
|
427
|
+
|
|
428
|
+
1. **Spec completeness** — all 11 operations, both transport bindings, spec-compliant agent card path
|
|
429
|
+
2. **Production storage** — SQLite store ships out of the box; no extra code to write
|
|
430
|
+
3. **Spec fidelity** — schema and routing auto-update from the canonical proto/JSON files
|
|
431
|
+
4. **Observability** — OpenTelemetry instrumentation built in
|
|
432
|
+
5. **Multi-turn support** — `STATE_INPUT_REQUIRED` state machine entry is formally modeled
|
|
433
|
+
6. **Push notification infrastructure** — full config CRUD + async webhook delivery
|
|
434
|
+
7. **Example breadth** — async jobs, multi-turn, multi-agent, push notifications all demonstrated
|
|
435
|
+
|
|
436
|
+
### Where `simple_a2a` wins
|
|
437
|
+
|
|
438
|
+
1. **Readability** — every class is short, explicit, and readable without spec knowledge
|
|
439
|
+
2. **Testability** — abstract `AgentExecutor` is easy to test in isolation; 98% coverage
|
|
440
|
+
3. **Protocol versioning** — `A2A-Version` header negotiation not present in `agent2agent`
|
|
441
|
+
4. **Multi-agent isolation** — path-based isolation via `Rack::URLMap` is architecturally cleaner
|
|
442
|
+
5. **Client ergonomics** — async context awareness; returns typed model objects
|
|
443
|
+
6. **JWT push auth** — RS256 JWT signing for webhook delivery is more sophisticated
|
|
444
|
+
7. **Lighter binary footprint** — no native protobuf or SQLite extensions required for the base gem
|
|
445
|
+
8. **Conventional test structure** — Minitest + simplecov is standard, CI-friendly
|
|
446
|
+
|
|
447
|
+
### The fundamental trade-off
|
|
448
|
+
|
|
449
|
+
`agent2agent` trades complexity for spec completeness and auto-synchronization with the A2A specification. It's the right choice for teams that need the full protocol surface and are running under Falcon with Async throughout.
|
|
450
|
+
|
|
451
|
+
`simple_a2a` trades spec completeness for simplicity and explicitness. It's the right choice when you want to understand every line of your A2A dependency, need conventional testing tools, or are embedding an A2A server into an existing Ruby application without committing to a full fiber-based async stack at every layer.
|
|
452
|
+
|
|
453
|
+
### Gaps in `simple_a2a` worth closing
|
|
454
|
+
|
|
455
|
+
1. **Agent card path** — `/agentCard` should be `/.well-known/agent-card.json` for spec compliance
|
|
456
|
+
2. **REST transport binding** — currently JSON-RPC only; REST endpoints would enable broader interop
|
|
457
|
+
3. **`SubscribeToTask`** — SSE stream on an existing task is missing; only `sendSubscribe` (new task + stream) is supported
|
|
458
|
+
4. **`GetExtendedAgentCard`** — not implemented
|
|
459
|
+
5. **Push notification config persistence** — `PushSender` exists but config CRUD stubs don't store anything
|
|
460
|
+
6. **Persistent storage** — `Storage::Base` interface is ready; a SQLite or file-backed implementation would make `simple_a2a` production-ready without adding native extensions
|
data/docs/api/client/index.md
CHANGED
|
@@ -111,6 +111,25 @@ Events are instances of:
|
|
|
111
111
|
|
|
112
112
|
The stream ends when the server sends a `final: true` event. The block is not called for malformed or comment-only SSE frames.
|
|
113
113
|
|
|
114
|
+
### `#resubscribe(task_id:, &block)`
|
|
115
|
+
|
|
116
|
+
Attaches an SSE stream to an already-running task. The first event yielded to the block is the current Task snapshot (a plain `Hash` — no `type` field); subsequent events are the live stream.
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
client.resubscribe(task_id: "existing-task-id") do |event|
|
|
120
|
+
case event
|
|
121
|
+
when Hash
|
|
122
|
+
puts "reconnected — state: #{event['status']['state']}"
|
|
123
|
+
when A2A::Models::TaskStatusUpdateEvent
|
|
124
|
+
puts "Status: #{event.status.state} (final=#{event.final})"
|
|
125
|
+
when A2A::Models::TaskArtifactUpdateEvent
|
|
126
|
+
puts "Artifact: #{event.artifact.parts.map(&:text).join}"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Returns `UnsupportedOperationError` (via `A2A::Error`) if the task is terminal or not currently streaming.
|
|
132
|
+
|
|
114
133
|
### Using inside an Async reactor
|
|
115
134
|
|
|
116
135
|
Both `Base` and `SSE` detect whether they're already inside an `Async` reactor via `Async::Task.current?`. Inside a reactor, they call the underlying `Async::HTTP::Internet` directly. Outside, they wrap the call in `Async { }.wait`.
|
data/docs/api/index.md
CHANGED
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
```ruby
|
|
16
16
|
A2A.logger = Logger.new($stdout) # optional — logs internal warnings
|
|
17
17
|
|
|
18
|
-
A2A.server(**opts)
|
|
19
|
-
A2A.
|
|
20
|
-
A2A.
|
|
18
|
+
A2A.server(**opts) # → A2A::Server::Base.new(**opts)
|
|
19
|
+
A2A.multi_server(**opts) # → A2A::Server::MultiAgent.new(**opts)
|
|
20
|
+
A2A.client(**opts) # → A2A::Client::Base.new(**opts)
|
|
21
|
+
A2A.sse_client(**opts) # → A2A::Client::SSE.new(**opts)
|
|
21
22
|
```
|
|
22
23
|
|
|
23
24
|
## Constants
|
data/docs/api/models/index.md
CHANGED
|
@@ -68,13 +68,14 @@ A2A::Models::Artifact.new(
|
|
|
68
68
|
|
|
69
69
|
| Attribute | Type | Description |
|
|
70
70
|
|---|---|---|
|
|
71
|
+
| `artifact_id` | String | UUID, auto-generated |
|
|
71
72
|
| `name` | String | Artifact identifier |
|
|
72
73
|
| `description` | String | Human-readable description |
|
|
73
74
|
| `parts` | [Part] | Content parts |
|
|
74
|
-
| `index` | Integer | Position in artifact sequence |
|
|
75
|
-
| `append` | Boolean | True if this is a streaming chunk appended to a prior artifact |
|
|
76
|
-
| `last_chunk` | Boolean | True if this is the final streaming chunk |
|
|
77
75
|
| `metadata` | Hash | Arbitrary metadata |
|
|
76
|
+
| `extensions` | Array | Protocol extension data |
|
|
77
|
+
|
|
78
|
+
Note: `append` and `last_chunk` are **not** Artifact attributes — they are parameters on `ctx.emit_artifact(artifact, append:, last_chunk:)` and on `TaskArtifactUpdateEvent`.
|
|
78
79
|
|
|
79
80
|
---
|
|
80
81
|
|
|
@@ -160,16 +161,16 @@ card = A2A::Models::AgentCard.new(
|
|
|
160
161
|
|---|---|---|---|
|
|
161
162
|
| `streaming` | Boolean | false | Supports `tasks/sendSubscribe` |
|
|
162
163
|
| `push_notifications` | Boolean | false | Supports webhook push |
|
|
163
|
-
| `
|
|
164
|
+
| `extended_agent_card` | Boolean | false | Exposes extended AgentCard at `GET /agentCard?extended=true` |
|
|
164
165
|
|
|
165
166
|
### AgentSkill
|
|
166
167
|
|
|
167
|
-
| Attribute | Description |
|
|
168
|
-
|
|
169
|
-
| `name` | Skill identifier |
|
|
170
|
-
| `description` | Human-readable description |
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
168
|
+
| Attribute | Required | Description |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `name` | yes | Skill identifier |
|
|
171
|
+
| `description` | | Human-readable description |
|
|
172
|
+
| `input_schema` | | JSON Schema describing accepted input |
|
|
173
|
+
| `output_schema` | | JSON Schema describing produced output |
|
|
173
174
|
|
|
174
175
|
### AgentInterface
|
|
175
176
|
|
|
@@ -218,7 +219,8 @@ A2A::Models::TaskArtifactUpdateEvent.new(
|
|
|
218
219
|
config = A2A::Models::PushNotificationConfig.new(
|
|
219
220
|
webhook_url: "https://example.com/hook",
|
|
220
221
|
authentication_info: A2A::Models::AuthenticationInfo.new(
|
|
221
|
-
scheme: "bearer"
|
|
222
|
+
scheme: "bearer",
|
|
223
|
+
value: "" # not used for JWT; PushSender generates the token from the private key
|
|
222
224
|
)
|
|
223
225
|
)
|
|
224
226
|
config.valid? # => true
|
data/docs/api/server/index.md
CHANGED
|
@@ -52,7 +52,6 @@ Each entry in `agents` accepts the same core configuration used by `Server::Base
|
|
|
52
52
|
| `:agent_card` | Yes | AgentCard returned by that path's `/agentCard` endpoint |
|
|
53
53
|
| `:executor` | Yes | Executor that handles requests for that path |
|
|
54
54
|
| `:storage` | No | Storage backend for that path; defaults to `A2A::Storage::Memory.new` |
|
|
55
|
-
| `:event_router` | No | SSE event router for that path; defaults to a new router |
|
|
56
55
|
| `:push_sender` | No | Push notification sender for that path |
|
|
57
56
|
|
|
58
57
|
For a runnable example, see the [Multi-Agent LLM Research demo](../../examples/llm-research.md).
|
|
@@ -93,7 +92,7 @@ Passed to `AgentExecutor#call`. Provides access to the request and helper method
|
|
|
93
92
|
ctx.task # => A2A::Models::Task
|
|
94
93
|
ctx.message # => A2A::Models::Message (the incoming message)
|
|
95
94
|
ctx.storage # => A2A::Storage::Base
|
|
96
|
-
ctx.event_router # => A2A::Server::
|
|
95
|
+
ctx.event_router # => A2A::Server::TaskBroadcast (duck-typed — responds to #publish)
|
|
97
96
|
ctx.config # => Hash (arbitrary per-request config, default {})
|
|
98
97
|
|
|
99
98
|
ctx.save_task # persists task to storage
|
|
@@ -114,17 +113,50 @@ ctx.resume_message # => A2A::Models::Message (the new input from the user)
|
|
|
114
113
|
|
|
115
114
|
---
|
|
116
115
|
|
|
117
|
-
## Server::
|
|
116
|
+
## Server::TaskBroadcast
|
|
118
117
|
|
|
119
|
-
|
|
118
|
+
Per-task lock-free SSE fan-out. One `TaskBroadcast` is created per streaming task and held in the `BroadcastRegistry` for the duration of that task. You rarely interact with this directly — use `ctx.emit_status` and `ctx.emit_artifact` instead.
|
|
120
119
|
|
|
121
120
|
```ruby
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
broadcast = A2A::Server::TaskBroadcast.new
|
|
122
|
+
|
|
123
|
+
queue = broadcast.subscribe # returns a RactorQueue for this subscriber
|
|
124
|
+
broadcast.publish(task_id, event) # fans event out to all subscriber queues
|
|
125
|
+
broadcast.error("something failed") # fans a BroadcastError sentinel to all queues
|
|
126
|
+
broadcast.close # fans the DONE sentinel — signals end of stream
|
|
127
|
+
broadcast.unsubscribe(queue) # removes one subscriber
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Each subscriber gets its own `RactorQueue`. `async_push` / `async_pop` cooperate with the Falcon fiber scheduler via `sleep(0)`.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Server::BroadcastRegistry
|
|
135
|
+
|
|
136
|
+
Thread-safe `task_id → TaskBroadcast` map, held at the App class level and shared across all concurrent requests.
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
registry = A2A::Server::BroadcastRegistry.new
|
|
140
|
+
registry.register(task_id, broadcast) # called when a streaming task starts
|
|
141
|
+
registry.find(task_id) # => TaskBroadcast or nil
|
|
142
|
+
registry.unregister(task_id) # called when the executor finishes
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`tasks/resubscribe` and `tasks/cancel` use `registry.find` to locate the live broadcast for a running task.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Server::PushConfigStore
|
|
150
|
+
|
|
151
|
+
In-memory store for push notification configurations, keyed by task ID. One instance is created per `Server::App` and exposed as `App.push_config_store`. You rarely interact with this directly — the four `tasks/pushNotification/*` handlers use it automatically.
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
store = A2A::Server::PushConfigStore.new
|
|
155
|
+
|
|
156
|
+
store.set(task_id, config) # => config — stores or replaces the config for this task
|
|
157
|
+
store.get(task_id) # => PushNotificationConfig or nil
|
|
158
|
+
store.delete(task_id) # => the deleted config or nil
|
|
159
|
+
store.list # => { task_id => config, … } snapshot
|
|
128
160
|
```
|
|
129
161
|
|
|
130
162
|
---
|
data/docs/api/storage/index.md
CHANGED
|
@@ -8,7 +8,6 @@ Abstract interface. Subclass this to implement custom storage backends.
|
|
|
8
8
|
class MyStorage < A2A::Storage::Base
|
|
9
9
|
def save(task) = … # persist, return task
|
|
10
10
|
def find(id) = … # return task or nil
|
|
11
|
-
def find!(id) = … # return task or raise A2A::TaskNotFoundError
|
|
12
11
|
def delete(id) = … # remove task
|
|
13
12
|
def list = … # return array of all tasks
|
|
14
13
|
end
|