KubeMQ
AiwayAI Agents (A2A)

How It Works

Inside the A2A gateway — the virtual-subscriber bridge, internal channels, header forwarding, concurrency, and cluster behavior.

The A2A connector is a transparent JSON-RPC 2.0 proxy. It never runs your agent's logic — it routes a request to the right agent, waits for the reply, and relays it back unchanged. The piece that makes agents plain HTTP servers (no KubeMQ SDK) is the virtual subscriber: a per-agent internal bridge that turns a Query into an outbound HTTP POST.

Overview

When a caller hits POST /a2a/<agent_id>, the request does not go straight to your agent over HTTP. Instead it is published as a Query on the agent's internal channel, picked up by that agent's virtual subscriber, and forwarded to the agent's registered URL as an HTTP POST. The agent's response travels the same path in reverse.

This indirection is what lets agents stay simple — they are standard A2A-compliant HTTP servers with zero KubeMQ dependencies (no broker client, no protobuf, no client library) — while still benefiting from KubeMQ's routing, authorization, metrics, and persistence infrastructure. For why agents are plain HTTP URLs, see the overview.

The components

The gateway is one of several cooperating parts. The A2A connector handles the protocol; the Agent Registry tracks who is registered; the Subscriber Manager owns the lifecycle of every virtual subscriber; and the Replicator keeps registry state in sync across a cluster.

The A2A connector proxies via the broker; a per-agent virtual subscriber bridges the Query to an HTTP POST.

ComponentRole
A2A connectorTransparent JSON-RPC 2.0 proxy; agent-management REST API; SSE relay
Agent RegistrySQLite-backed store of registered agents with TTL liveness and cluster replication
Subscriber ManagerCreates/destroys virtual subscribers on registration, deregistration, and TTL expiry
Virtual SubscriberPer-agent broker-to-HTTP bridge; the reason agents need no KubeMQ SDK
ReplicatorPropagates registry changes across cluster nodes over Events Store

The virtual-subscriber bridge

Each registered agent gets its own virtual subscriber — an internal broker client that subscribes to Queries on the agent's channel and forwards them to the agent over HTTP. It is spawned by the Subscriber Manager when the agent registers and torn down when the agent deregisters or its TTL expires.

When a Query arrives, the virtual subscriber:

Extracts the JSON-RPC body from the broker request.
Unpacks forwarded HTTP headers (the a2a_hdr_* tags) back into real headers.
Sets the X-KubeMQ-Caller-ID header to the original caller's identity.
Makes an HTTP POST to the agent's registered URL with Content-Type: application/json.
Wraps the agent's HTTP response and publishes it back on the broker reply channel.

The subscriber dispatches on the a2a_method tag in the incoming Query: message/stream is routed to the streaming handler (which opens an SSE connection to the agent), stream_cancel is routed to the cancel handler, and everything else — including message/send and tasks/send — goes to the synchronous request handler. All broker operations for an agent use the client ID a2a-vsub-<agent_id> (for example a2a-vsub-agent-b).

Virtual subscribers use a queue subscription with the queue group set to the agent ID, so load-balanced delivery works correctly across the cluster.

Internal channels

All agent-platform traffic uses channels under the reserved _AGENTS_. prefix. User channels that start with _AGENTS_. are rejected by IsReservedChannel — see Auth & security.

ChannelPurposeTransport
_AGENTS_.agents/<agent_id>Request/reply to the agent via its virtual subscriber (including stream cancel)Query
_AGENTS_.stream/<stream_id>Temporary SSE stream events relayed by the virtual subscriberEvents
_AGENTS_.discoveryRegistry replication events across cluster nodesEvents Store

Header forwarding

Caller HTTP headers ride along to the agent through the broker request's Tags field using an a2a_hdr_ prefix: the connector packs each allowed header as a2a_hdr_<Header-Name>: <value>, and the virtual subscriber strips the prefix and replays it as a real HTTP header on the outbound POST.

Hop-by-hop and sensitive headers are never forwarded: Connection, Keep-Alive, Transfer-Encoding, Te, Trailer, Upgrade, Host, Content-Length, Authorization, Cookie, Set-Cookie, Proxy-Authorization, Proxy-Authenticate, X-Forwarded-For, and X-Real-Ip are dropped.

Regardless of which headers the caller sent, the virtual subscriber always sets X-KubeMQ-Caller-ID on the outbound request, carrying the original caller's KubeMQ client identity — so your agent can always tell who called it. Any KubeMQ transport (gRPC, REST, the A2A HTTP gateway, or the MCP bridge) can forward headers the same way by setting a2a_hdr_* tags on the request.

Concurrency control

Each virtual subscriber enforces a per-agent concurrency cap using a buffered-channel semaphore sized to AgentMaxConcurrency (default 100). When a Query arrives and the semaphore has capacity, a handler goroutine runs and releases its slot on completion. When all slots are occupied, the overflow Query is immediately rejected with a transport failure — Executed: false, Error: "server busy: concurrency limit reached" — without spawning a goroutine.

This keeps a single hot agent from consuming unbounded goroutines under load. The cap, the response-size limit, and the timeout/gateway-buffer behavior are covered in detail in Concurrency & limits.

Timeouts and the gateway buffer

Each request carries a timeout taken from params.configuration.timeout in the JSON-RPC body, falling back to DefaultTimeoutSeconds (300) and capped at MaxTimeoutSeconds (3600). Before forwarding, the gateway adds a GatewayTimeoutBuffer of 10 seconds to the downstream deadline so the gateway never times out before the agent does. SSE stream endpoints skip the per-route timeout middleware entirely because they are long-lived; they are bounded by the idle timeout instead (see Streaming).

Cluster behavior

Virtual subscribers are local to the node where the agent registered — they are not replicated. Registry state, however, is shared cluster-wide.

Only the registering node spawns the virtual subscriber; other nodes store the card and route Queries to it over the cluster mesh.

  • An agent registers on Node A → its virtual subscriber is spawned on Node A only.
  • Replication events over _AGENTS_.discovery propagate the AgentCard to Nodes B and C; they store the card but do not spawn a subscriber.
  • The cluster mesh routes Queries from any node to the virtual subscriber on Node A.
  • If Node A goes down, the agent's heartbeat expires on all nodes and its subscribers are cleaned up.
  • When the agent re-registers on Node B, a fresh virtual subscriber is spawned there.

Transport vs. application errors

Because the bridge is the boundary between the broker and HTTP, it cleanly separates two failure classes via the Executed flag on the response:

  • Transport error (Executed: false) — the agent never processed the request: it was unreachable, timed out, returned 502/503/504, or its response exceeded the size cap. Safe to retry.
  • Application error (Executed: true) — the agent did process the request and returned an HTTP 4xx/5xx with a JSON-RPC error body. Retrying blindly usually will not help.

This distinction lets callers choose the right retry strategy — see Error handling.

Was this page helpful?

On this page