Skip to content

Commit feb0baf

Browse files
Document Queued A2A adapter in README and /docs
Add docs/protocol-queued-a2a.md covering: broker technologies (RabbitMQ, Azure Service Bus), the QueuedAgentCard model and queueEndpoint fields, design decisions (why ProtocolType stays A2A, separate adapter rationale, credentials-not-stored policy, skills round-trip), and registration flow examples for both broker types with KEDA liveness patterns. Update README: - Add QueuedA2A to protocol support section with API surface summary - Extend Queue-backed agents section to show both registration paths - Add /a2a/async/agents rows to the API overview table - Update auth section's public-endpoint list - Update project structure to reflect QueuedA2A/ folders - Add link to new docs page alongside A2A/MCP/ACP Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0554483 commit feb0baf

2 files changed

Lines changed: 305 additions & 10 deletions

File tree

README.md

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ AgentRegistry is the connective tissue. An agent registers itself when it starts
2222
│ AgentRegistry.Api │
2323
│ ASP.NET Core 10 · Minimal APIs · Scalar UI · OpenTelemetry │
2424
│ Auth: API key (Admin/Agent scopes) + JWT Bearer │
25-
│ Protocol adapters: A2A · MCP
25+
│ Protocol adapters: A2A · MCP · ACP · Queued A2A
2626
└────────────┬───────────────────────┬────────────────────────────┘
2727
│ │
2828
┌────────────▼───────┐ ┌───────────▼────────────────────────────┐
@@ -60,6 +60,7 @@ Detailed design rationale for each adapter is in [`/docs`](docs/):
6060
- [A2A adapter design](docs/protocol-a2a.md)
6161
- [MCP adapter design](docs/protocol-mcp.md)
6262
- [ACP adapter design](docs/protocol-acp.md)
63+
- [Queued A2A adapter design](docs/protocol-queued-a2a.md)
6364

6465
### A2A (Agent-to-Agent)
6566

@@ -96,6 +97,18 @@ Targets [ACP spec 0.2.0](https://github.com/i-am-bee/acp) (IBM Research / BeeAI)
9697

9798
The manifest carries MIME-typed content types, JSON Schema for input/output/config/thread state, performance status metrics, and rich metadata (framework, natural languages, license, author). All fields round-trip through `Endpoint.ProtocolMetadata`. Agent names are normalised to RFC 1123 DNS-label format on manifest generation.
9899

100+
### Queued A2A (A2A over async message brokers)
101+
102+
Agents that communicate via **RabbitMQ** or **Azure Service Bus** rather than HTTP. The A2A wire protocol (task request / status update / result message shapes) is unchanged — only the transport differs. Callers publish A2A task messages to a named topic on the broker and receive responses asynchronously, enabling KEDA-scaled workers, long-running tasks, and decoupled agent pipelines.
103+
104+
- `GET /a2a/async/agents/{id}` — queued A2A agent card with full broker connection details (public)
105+
- `GET /a2a/async/agents` — filtered list of queued agents (public)
106+
- `POST /a2a/async/agents` — register by submitting a queued agent card (Agent or Admin)
107+
108+
The `queueEndpoint` object in each card carries `technology` (`"rabbitmq"` or `"azure-service-bus"`), `host`, `port`, `virtualHost`, `exchange`, `taskTopic` (where callers publish), and `responseTopic` (where callers listen for replies). For Azure Service Bus, `namespace` and `entityPath` replace the AMQP-specific fields.
109+
110+
Queued agents also appear in `GET /discover/agents?protocol=A2A` alongside HTTP A2A agents since they share the same protocol type. See [Queued A2A adapter design](docs/protocol-queued-a2a.md) for the full design rationale.
111+
99112
### Generic (protocol-agnostic)
100113

101114
All protocols can also be registered and discovered through the generic API, which returns the registry's own domain model rather than protocol-native card formats.
@@ -230,7 +243,7 @@ The registry accepts two authentication methods, selected by header:
230243
- A `registry_scope` claim with value `Admin` or `Agent`
231244
- A `roles` claim with value `registry.admin` or `registry.agent`
232245

233-
Discovery, the MCP server endpoint, and protocol card endpoints (`/discover/agents`, `/mcp`, `/a2a/agents/*`, `/mcp/servers/*`) are always public — no auth required.
246+
Discovery, the MCP server endpoint, and protocol card endpoints (`/discover/agents`, `/mcp`, `/a2a/agents/*`, `/a2a/async/agents/*`, `/mcp/servers/*`, `/acp/agents/*`) are always public — no auth required.
234247

235248
## API overview
236249

@@ -273,6 +286,14 @@ Discovery, the MCP server endpoint, and protocol card endpoints (`/discover/agen
273286
| `GET` | `/acp/agents` | Public | Filtered list of ACP agent manifests |
274287
| `POST` | `/acp/agents` | Agent or Admin | Register by submitting an ACP agent manifest |
275288

289+
### Queued A2A protocol
290+
291+
| Method | Path | Auth | Description |
292+
|---|---|---|---|
293+
| `GET` | `/a2a/async/agents/{id}` | Public | Queued A2A card with broker connection details |
294+
| `GET` | `/a2a/async/agents` | Public | Filtered list of queued A2A agent cards |
295+
| `POST` | `/a2a/async/agents` | Agent or Admin | Register an agent with queue endpoint details |
296+
276297
### Key management and system
277298

278299
| Method | Path | Auth | Description |
@@ -354,20 +375,53 @@ The two services are non-conflicting — if a config-defined agent also self-reg
354375

355376
## Queue-backed agents
356377

357-
Agents using AMQP or Azure Service Bus don't need to be running when discovered. The registry stores the queue address as the endpoint:
378+
Agents using AMQP or Azure Service Bus don't need to be running when discovered. The registry stores the queue address as the endpoint. There are two ways to register a queue-backed agent:
379+
380+
### Queued A2A (recommended for A2A agents over a broker)
381+
382+
Use `POST /a2a/async/agents` with the full `queueEndpoint` connection details:
383+
384+
```json
385+
{
386+
"name": "ResearchAgent",
387+
"description": "On-demand research agent",
388+
"version": "1.0",
389+
"skills": [{ "id": "research", "name": "Research", "description": "Researches a topic", "tags": ["search"] }],
390+
"defaultInputModes": ["application/json"],
391+
"defaultOutputModes": ["application/json"],
392+
"queueEndpoint": {
393+
"technology": "rabbitmq",
394+
"host": "rabbitmq.example.com",
395+
"port": 5672,
396+
"virtualHost": "/",
397+
"exchange": "rockbot",
398+
"taskTopic": "agent.task.ResearchAgent",
399+
"responseTopic": "agent.response.{callerName}"
400+
}
401+
}
402+
```
403+
404+
Clients discover the agent via `GET /a2a/async/agents` and receive the full broker connection details needed to publish A2A task messages directly.
405+
406+
### Generic registration
407+
408+
Use `POST /agents` with explicit `transport` and `protocol` fields. This works for any protocol/transport combination but returns the registry's internal model rather than a protocol-native card:
358409

359410
```json
360411
{
361412
"name": "async-processor",
362-
"transport": "AzureServiceBus",
363-
"protocol": "A2A",
364-
"address": "agents/summarizer/requests",
365-
"livenessModel": "Ephemeral",
366-
"ttlSeconds": 60
413+
"endpoints": [{
414+
"name": "queue",
415+
"transport": "AzureServiceBus",
416+
"protocol": "A2A",
417+
"address": "agents/summarizer/requests",
418+
"livenessModel": "Ephemeral",
419+
"ttlSeconds": 60
420+
}]
367421
}
368422
```
369423

370-
A KEDA-scaled worker registers on startup, processes jobs, and the TTL expires naturally when the scaling group idles. Consumers route work to the queue address — whether the worker is currently running or not is KEDA's concern.
424+
In both cases, a KEDA-scaled worker registers on startup, processes jobs, and the TTL expires naturally when the scaling group idles. Consumers route work to the queue address — whether the worker is currently running or not is KEDA's concern. See [Queued A2A adapter design](docs/protocol-queued-a2a.md) for the full pattern including liveness and round-tripping.
371425

372426
## Configuration reference
373427

@@ -429,9 +483,10 @@ src/
429483
AgentRegistry.Infrastructure/ EF Core (PostgreSQL), Redis, SQL API key service
430484
AgentRegistry.Api/ ASP.NET Core 10 minimal API, auth, Scalar
431485
Protocols/
432-
A2A/ A2A v1.0 RC agent card adapter
486+
A2A/ A2A v1.0 RC agent card adapter (HTTP)
433487
MCP/ MCP 2025-11-25 server card adapter (Streamable HTTP)
434488
ACP/ ACP 0.2.0 agent manifest adapter
489+
QueuedA2A/ A2A over async message brokers (RabbitMQ, Azure Service Bus)
435490
tests/
436491
AgentRegistry.Domain.Tests/ Domain unit tests
437492
AgentRegistry.Application.Tests/ Service tests using Rocks source-gen mocks
@@ -440,6 +495,7 @@ tests/
440495
A2A/ A2A endpoint tests
441496
MCP/ MCP endpoint tests
442497
ACP/ ACP endpoint tests
498+
QueuedA2A/ Queued A2A endpoint tests
443499
k8s/
444500
redis.yaml Redis StatefulSet + Service
445501
agentregistry/

docs/protocol-queued-a2a.md

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
# Queued A2A Protocol Adapter
2+
3+
## What is Queued A2A?
4+
5+
Queued A2A describes agents that use the **A2A wire protocol over an async message broker** instead of HTTP. The message format — task requests, status updates, results — is identical to standard A2A. What changes is the transport: callers publish messages to a queue or topic on a broker (RabbitMQ, Azure Service Bus) and receive responses asynchronously on a reply topic, rather than making a synchronous HTTP call.
6+
7+
This pattern is common in:
8+
9+
- **KEDA-scaled workers** — agents that are scaled to zero when idle. A KEDA trigger watches the queue depth; the worker spins up when messages arrive.
10+
- **Long-running tasks** — research, code generation, batch processing where a 30-second HTTP timeout is impractical.
11+
- **Decoupled pipelines** — agent chains where intermediate results pass through a broker, enabling retries, dead-lettering, and fan-out without tight coupling between agents.
12+
13+
The rockbot project demonstrates this pattern concretely: `SampleAgent` and `ResearchAgent` run as KEDA-scalable workers, communicate via RabbitMQ, and use A2A message shapes for all task exchange.
14+
15+
## Supported broker technologies
16+
17+
| Technology | `TransportType` | Key fields |
18+
|---|---|---|
19+
| RabbitMQ (AMQP 0-9-1) | `Amqp` | `host`, `port`, `virtualHost`, `exchange`, `taskTopic` |
20+
| Azure Service Bus | `AzureServiceBus` | `namespace`, `entityPath`, `taskTopic` |
21+
22+
## The QueuedAgentCard
23+
24+
The discovery and registration format is a `QueuedAgentCard` — an A2A agent card extended with a `queueEndpoint` object:
25+
26+
```json
27+
{
28+
"name": "ResearchAgent",
29+
"description": "On-demand research agent using web search and page fetching",
30+
"version": "1.0",
31+
"skills": [
32+
{
33+
"id": "research",
34+
"name": "Research",
35+
"description": "Research a topic using web search",
36+
"tags": ["search", "web"]
37+
}
38+
],
39+
"defaultInputModes": ["application/json"],
40+
"defaultOutputModes": ["application/json"],
41+
"queueEndpoint": {
42+
"technology": "rabbitmq",
43+
"host": "rabbitmq.example.com",
44+
"port": 5672,
45+
"virtualHost": "/",
46+
"exchange": "rockbot",
47+
"taskTopic": "agent.task.ResearchAgent",
48+
"responseTopic": "agent.response.{callerName}"
49+
},
50+
"id": "3fa85f64-...",
51+
"isLive": true
52+
}
53+
```
54+
55+
### `queueEndpoint` fields
56+
57+
| Field | Required | Description |
58+
|---|---|---|
59+
| `technology` | Yes | `"rabbitmq"` or `"azure-service-bus"` |
60+
| `taskTopic` | Yes | Routing key or topic path that callers publish task messages to |
61+
| `host` | RabbitMQ | Broker hostname (e.g. `rabbitmq.example.com`) |
62+
| `port` | No | Broker port; defaults to 5672 (AMQP) or 5671 (AMQPS) if omitted |
63+
| `virtualHost` | No | AMQP virtual host; typically `/` |
64+
| `exchange` | No | AMQP exchange name (topic exchange, e.g. `rockbot`) |
65+
| `responseTopic` | No | Pattern callers subscribe to for responses (e.g. `agent.response.{callerName}`) |
66+
| `namespace` | Azure SB | Fully-qualified Service Bus namespace (e.g. `mybus.servicebus.windows.net`) |
67+
| `entityPath` | Azure SB | Service Bus queue or topic path |
68+
69+
### Registry-added fields
70+
71+
`id` and `isLive` are added by the registry on discovery responses. Omit them when registering.
72+
73+
## API endpoints
74+
75+
| Method | Path | Auth | Description |
76+
|---|---|---|---|
77+
| `POST` | `/a2a/async/agents` | Agent or Admin | Register an agent with queue endpoint details |
78+
| `GET` | `/a2a/async/agents` | Public | List / discover queued agents (paginated) |
79+
| `GET` | `/a2a/async/agents/{id}` | Public | Retrieve a specific queued agent card |
80+
81+
The list endpoint supports the same query parameters as other protocol list endpoints: `capability`, `tags`, `liveOnly` (default `true`), `page`, `pageSize` (max 100).
82+
83+
## Design decisions
84+
85+
### ProtocolType stays A2A
86+
87+
The A2A message shapes (task request, status update, result) are unchanged. Only the transport layer differs. Using a new `ProtocolType` would fragment discovery — a consumer searching for A2A agents would miss queue-backed ones. Keeping `ProtocolType.A2A` means `GET /discover/agents?protocol=A2A` returns both HTTP and queued A2A agents.
88+
89+
`TransportType` distinguishes them: `Http` for classic A2A over HTTP, `Amqp` for RabbitMQ, `AzureServiceBus` for Azure.
90+
91+
### Separate adapter from the HTTP A2A adapter
92+
93+
The HTTP A2A adapter serves and accepts standard A2A agent cards where `supportedInterfaces[].url` is an HTTP URL. For queued agents, the relevant connection information (exchange, routing key, virtual host) doesn't fit cleanly into a URL field. A dedicated `/a2a/async/agents` surface with a `queueEndpoint` object is clearer for consumers than trying to encode broker details into a URL.
94+
95+
The existing `GET /a2a/agents/{id}` endpoint continues to work for HTTP A2A agents and will return a synthetic placeholder URL for any non-HTTP endpoints it encounters. The dedicated queued endpoint is the intended surface for agents that are truly queue-native.
96+
97+
### Connection details in ProtocolMetadata
98+
99+
All `queueEndpoint` fields and the full card (version, skills, I/O modes) are serialised into `Endpoint.ProtocolMetadata` at registration time. On discovery, the mapper deserialises them to reconstruct the original card exactly. This is the same round-trip strategy used by A2A, MCP, and ACP — no domain model changes required for new fields.
100+
101+
`Endpoint.Address` holds `taskTopic` — the routing key / entity path where callers publish. This is the primary "address" of the agent from the registry's perspective, analogous to an HTTP URL.
102+
103+
### No credentials in the card
104+
105+
Connection strings with usernames, passwords, or SAS tokens are not stored. The card contains only the structural connection details (host, port, exchange, topic). Callers are expected to hold broker credentials separately — via Kubernetes secrets, Azure Key Vault, or equivalent — and combine them with the structural details from the registry.
106+
107+
### Skills and domain capabilities
108+
109+
Skills in the `QueuedAgentCard` map directly to registry capabilities, enabling queued agents to be discovered through the generic `GET /discover/agents?capability=research` endpoint alongside HTTP agents. When an agent registers with multiple skills, all skills become capabilities in the domain model.
110+
111+
On discovery, the stored skills are returned verbatim (preserving original string IDs like `"research"` rather than internal Guid IDs) because they are read from `ProtocolMetadata`, not reconstructed from capabilities.
112+
113+
### isLive reflects heartbeat state
114+
115+
Queued agents typically use the `Persistent` liveness model and call `POST /agents/{id}/endpoints/{eid}/heartbeat` periodically. KEDA-scaled workers may use `Ephemeral` liveness — registering when a pod starts, calling `POST /agents/{id}/endpoints/{eid}/renew` on each task invocation, and relying on TTL expiry when the pod scales to zero.
116+
117+
`isLive: false` does not mean the agent's queue is unavailable — it means the registry has not seen a heartbeat or renewal within the grace period. Consumers can choose to route work to stale endpoints if they know the queue is durable.
118+
119+
## Registration flows
120+
121+
### RabbitMQ agent
122+
123+
```json
124+
POST /a2a/async/agents
125+
Authorization: X-Api-Key: ar_...
126+
127+
{
128+
"name": "ResearchAgent",
129+
"description": "On-demand research agent",
130+
"version": "1.0",
131+
"skills": [
132+
{ "id": "research", "name": "Research", "description": "Researches a topic", "tags": ["search"] }
133+
],
134+
"defaultInputModes": ["application/json"],
135+
"defaultOutputModes": ["application/json"],
136+
"queueEndpoint": {
137+
"technology": "rabbitmq",
138+
"host": "rabbitmq.prod.example.com",
139+
"port": 5672,
140+
"virtualHost": "/",
141+
"exchange": "rockbot",
142+
"taskTopic": "agent.task.ResearchAgent",
143+
"responseTopic": "agent.response.{callerName}"
144+
}
145+
}
146+
```
147+
148+
Response `201 Created`:
149+
```json
150+
{
151+
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
152+
"name": "ResearchAgent",
153+
"description": "On-demand research agent",
154+
"version": "1.0",
155+
"skills": [{ "id": "research", "name": "Research", "description": "Researches a topic", "tags": ["search"] }],
156+
"defaultInputModes": ["application/json"],
157+
"defaultOutputModes": ["application/json"],
158+
"queueEndpoint": {
159+
"technology": "rabbitmq",
160+
"host": "rabbitmq.prod.example.com",
161+
"port": 5672,
162+
"virtualHost": "/",
163+
"exchange": "rockbot",
164+
"taskTopic": "agent.task.ResearchAgent",
165+
"responseTopic": "agent.response.{callerName}"
166+
},
167+
"isLive": true
168+
}
169+
```
170+
171+
### Azure Service Bus agent
172+
173+
```json
174+
POST /a2a/async/agents
175+
{
176+
"name": "InvoiceProcessor",
177+
"description": "Processes invoice documents",
178+
"version": "1.0",
179+
"skills": [
180+
{ "id": "process-invoice", "name": "Process Invoice", "description": "Extracts and validates invoice data", "tags": ["finance", "ocr"] }
181+
],
182+
"defaultInputModes": ["application/json", "application/pdf"],
183+
"defaultOutputModes": ["application/json"],
184+
"queueEndpoint": {
185+
"technology": "azure-service-bus",
186+
"namespace": "mybus.servicebus.windows.net",
187+
"entityPath": "invoice-processor-tasks",
188+
"taskTopic": "invoice-processor-tasks",
189+
"responseTopic": "invoice-processor-responses"
190+
}
191+
}
192+
```
193+
194+
### Keeping liveness alive (KEDA worker pattern)
195+
196+
An ephemeral, KEDA-scaled worker registers on pod startup and renews on each task:
197+
198+
```bash
199+
# On pod startup — register with 5-minute TTL
200+
curl -X POST https://registry.example.com/a2a/async/agents \
201+
-H "X-Api-Key: $REGISTRY_KEY" \
202+
-d '{ "name": "ResearchAgent", ..., "livenessModel": "Ephemeral", "ttlSeconds": 300 }'
203+
204+
# Capture the endpoint ID from the response, then on each task invocation:
205+
curl -X POST https://registry.example.com/agents/$AGENT_ID/endpoints/$ENDPOINT_ID/renew \
206+
-H "X-Api-Key: $REGISTRY_KEY"
207+
```
208+
209+
When the pod scales to zero, the TTL expires and the agent is no longer returned in `liveOnly=true` queries. The queue address in the card remains valid — KEDA will scale a new pod when messages arrive.
210+
211+
For persistent workers, use `heartbeatIntervalSeconds` instead and call `POST .../heartbeat` on schedule.
212+
213+
## Discovering queued agents
214+
215+
```bash
216+
# All live queued A2A agents
217+
GET /a2a/async/agents
218+
219+
# Filter by capability
220+
GET /a2a/async/agents?capability=research&liveOnly=true
221+
222+
# Filter by tag
223+
GET /a2a/async/agents?tags=finance,ocr
224+
```
225+
226+
Discovery returns a paginated list:
227+
228+
```json
229+
{
230+
"agents": [...],
231+
"totalCount": 12,
232+
"page": 1,
233+
"pageSize": 20,
234+
"totalPages": 1,
235+
"hasNextPage": false
236+
}
237+
```
238+
239+
Queued agents also appear in the generic discovery endpoint (`GET /discover/agents?protocol=A2A`) alongside HTTP A2A agents, since they share `ProtocolType.A2A`.

0 commit comments

Comments
 (0)