# Multi-Agent Gateway (/aiway/a2a/scenarios/multi-agent-gateway)



This scenario stands up **three independent agents** — echo, translate, and summarize —
behind a single A2A gateway, then drives them from one client: discover the right agent
by **skill tag**, and dispatch a `message/send&#x60; to it by &#x2A;*`agent_id`**. It ties together
the [registry](/aiway/a2a/registry) and
[synchronous messaging](/aiway/a2a/sync-messaging).

## The setup [#the-setup]

Each agent is a plain HTTP server registered by URL — no KubeMQ SDK runs on any of them.
The gateway keeps one [virtual subscriber](/aiway/a2a/architecture) per agent,
so a caller reaches any agent through the same `POST /a2a/{agent_id}` surface. Callers
never address an agent's HTTP URL directly; they address its `agent_id` and the gateway
routes the request.

<Mermaid
  chart="`
graph LR
CLIENT[&#x22;Client<br/>(router)&#x22;]
GW[&#x22;A2A Gateway<br/>:9090&#x22;]
REG[&#x22;Agent Registry&#x22;]
BROKER[&#x22;Message Broker&#x22;]
A1[&#x22;echo-agent<br/>skills: echo&#x22;]
A2[&#x22;translate-agent<br/>skills: translate, nlp&#x22;]
A3[&#x22;summarize-agent<br/>skills: summarize, nlp&#x22;]

CLIENT -- &#x22;GET /agents?skill_tags=nlp&#x22; --> REG
CLIENT -- &#x22;POST /a2a/translate-agent-01&#x22; --> GW
GW --> BROKER
BROKER -. &#x22;_AGENTS_.agents/translate-agent-01&#x22; .-> A2
GW -. &#x22;echo route&#x22; .-> A1
GW -. &#x22;summarize route&#x22; .-> A3

class CLIENT client
class GW,REG aiway
class BROKER broker
class A1,A2,A3 external
`"
/>

*One gateway fronts many agents; the client discovers by skill tag and routes by `agent_id`.*

## Step 1 — Register the agents [#step-1--register-the-agents]

Each agent registers its own [agent card](/aiway/a2a/agent-cards) with a unique
`agent_id`, its HTTP `url`, and a `skills` list. The `tags` on each skill are what make the
agent discoverable later. Agents that share a capability (here, `translate` and `summarize`
both carry the `nlp` tag) can be found together.

<Tabs groupId="language" items="['curl','C#','Go','Java','Python','TypeScript']">
  <Tab value="curl">
    ```bash
    # Register three agents with distinct skills.
    # (Each agent process registers itself on start; shown here as explicit calls.)
    curl -X POST http://localhost:9090/agents/register \
      -H "Content-Type: application/json" \
      -d '{
        "agent_id": "echo-agent-01",
        "name": "Echo Agent",
        "url": "http://localhost:18081/",
        "skills": [{"id": "echo", "name": "Echo", "tags": ["echo"]}]
      }'

    curl -X POST http://localhost:9090/agents/register \
      -H "Content-Type: application/json" \
      -d '{
        "agent_id": "translate-agent-01",
        "name": "Translate Agent",
        "url": "http://localhost:18082/",
        "skills": [{"id": "translate", "name": "Translate", "tags": ["translate", "nlp"]}]
      }'

    curl -X POST http://localhost:9090/agents/register \
      -H "Content-Type: application/json" \
      -d '{
        "agent_id": "summarize-agent-01",
        "name": "Summarize Agent",
        "url": "http://localhost:18083/",
        "skills": [{"id": "summarize", "name": "Summarize", "tags": ["summarize", "nlp"]}]
      }'
    ```
  </Tab>

  <Tab value="C#">
    ```csharp
    using System.Text;
    using System.Text.Json.Nodes;

    const string KubeMqUrl = "http://localhost:9090";
    using var client = new HttpClient();

    var agents = new[]
    {
        ("echo-agent-01", "Echo Agent", 18081, "echo", new[] { "echo" }),
        ("translate-agent-01", "Translate Agent", 18082, "translate", new[] { "translate", "nlp" }),
        ("summarize-agent-01", "Summarize Agent", 18083, "summarize", new[] { "summarize", "nlp" }),
    };

    foreach (var (id, name, port, skillId, tags) in agents)
    {
        var card = new JsonObject
        {
            ["agent_id"] = id,
            ["name"] = name,
            ["url"] = $"http://localhost:{port}/",
            ["skills"] = new JsonArray(new JsonObject
            {
                ["id"] = skillId,
                ["name"] = skillId,
                ["tags"] = new JsonArray(tags.Select(t => (JsonNode)t!).ToArray())
            })
        };
        var resp = await client.PostAsync(
            $"{KubeMqUrl}/agents/register",
            new StringContent(card.ToJsonString(), Encoding.UTF8, "application/json"));
        Console.WriteLine($"Registered {id}: {(int)resp.StatusCode}");
    }
    ```
  </Tab>

  <Tab value="Go">
    ```go
    package main

    import (
    	"bytes"
    	"encoding/json"
    	"fmt"
    	"net/http"
    )

    const kubemqURL = "http://localhost:9090"

    func main() {
    	agents := []map[string]interface{}{
    		{
    			"agent_id": "echo-agent-01", "name": "Echo Agent",
    			"url":    "http://localhost:18081/",
    			"skills": []map[string]interface{}{{"id": "echo", "name": "Echo", "tags": []string{"echo"}}},
    		},
    		{
    			"agent_id": "translate-agent-01", "name": "Translate Agent",
    			"url":    "http://localhost:18082/",
    			"skills": []map[string]interface{}{{"id": "translate", "name": "Translate", "tags": []string{"translate", "nlp"}}},
    		},
    		{
    			"agent_id": "summarize-agent-01", "name": "Summarize Agent",
    			"url":    "http://localhost:18083/",
    			"skills": []map[string]interface{}{{"id": "summarize", "name": "Summarize", "tags": []string{"summarize", "nlp"}}},
    		},
    	}
    	for _, card := range agents {
    		data, _ := json.Marshal(card)
    		resp, err := http.Post(kubemqURL+"/agents/register", "application/json", bytes.NewReader(data))
    		if err != nil {
    			fmt.Printf("Register %v failed: %v\n", card["agent_id"], err)
    			continue
    		}
    		resp.Body.Close()
    		fmt.Printf("Registered %v: %d\n", card["agent_id"], resp.StatusCode)
    	}
    }
    ```
  </Tab>

  <Tab value="Java">
    ```java
    import com.fasterxml.jackson.databind.ObjectMapper;

    import java.net.URI;
    import java.net.http.HttpClient;
    import java.net.http.HttpRequest;
    import java.net.http.HttpResponse;
    import java.util.List;
    import java.util.Map;

    public class Client {

        static final String KUBEMQ_URL = "http://localhost:9090";
        static final ObjectMapper MAPPER = new ObjectMapper();

        public static void main(String[] args) throws Exception {
            var client = HttpClient.newHttpClient();

            var agents = List.of(
                Map.of("agent_id", "echo-agent-01", "name", "Echo Agent",
                    "url", "http://localhost:18081/",
                    "skills", List.of(Map.of("id", "echo", "name", "Echo", "tags", List.of("echo")))),
                Map.of("agent_id", "translate-agent-01", "name", "Translate Agent",
                    "url", "http://localhost:18082/",
                    "skills", List.of(Map.of("id", "translate", "name", "Translate", "tags", List.of("translate", "nlp")))),
                Map.of("agent_id", "summarize-agent-01", "name", "Summarize Agent",
                    "url", "http://localhost:18083/",
                    "skills", List.of(Map.of("id", "summarize", "name", "Summarize", "tags", List.of("summarize", "nlp"))))
            );

            for (var card : agents) {
                var req = HttpRequest.newBuilder()
                    .uri(URI.create(KUBEMQ_URL + "/agents/register"))
                    .header("Content-Type", "application/json")
                    .POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(card)))
                    .build();
                var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
                System.out.println("Registered " + card.get("agent_id") + ": " + resp.statusCode());
            }
        }
    }
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import asyncio

    import httpx

    KUBEMQ_URL = "http://localhost:9090"

    AGENTS = [
        {"agent_id": "echo-agent-01", "name": "Echo Agent", "url": "http://localhost:18081/",
         "skills": [{"id": "echo", "name": "Echo", "tags": ["echo"]}]},
        {"agent_id": "translate-agent-01", "name": "Translate Agent", "url": "http://localhost:18082/",
         "skills": [{"id": "translate", "name": "Translate", "tags": ["translate", "nlp"]}]},
        {"agent_id": "summarize-agent-01", "name": "Summarize Agent", "url": "http://localhost:18083/",
         "skills": [{"id": "summarize", "name": "Summarize", "tags": ["summarize", "nlp"]}]},
    ]


    async def main() -> None:
        async with httpx.AsyncClient() as client:
            for card in AGENTS:
                resp = await client.post(f"{KUBEMQ_URL}/agents/register", json=card)
                print(f"Registered {card['agent_id']}: {resp.status_code}")


    if __name__ == "__main__":
        asyncio.run(main())
    ```
  </Tab>

  <Tab value="TypeScript">
    ```typescript
    const KUBEMQ_URL = "http://localhost:9090";

    const AGENTS = [
      { agent_id: "echo-agent-01", name: "Echo Agent", url: "http://localhost:18081/",
        skills: [{ id: "echo", name: "Echo", tags: ["echo"] }] },
      { agent_id: "translate-agent-01", name: "Translate Agent", url: "http://localhost:18082/",
        skills: [{ id: "translate", name: "Translate", tags: ["translate", "nlp"] }] },
      { agent_id: "summarize-agent-01", name: "Summarize Agent", url: "http://localhost:18083/",
        skills: [{ id: "summarize", name: "Summarize", tags: ["summarize", "nlp"] }] },
    ];

    async function main() {
      for (const card of AGENTS) {
        const resp = await fetch(`${KUBEMQ_URL}/agents/register`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(card),
        });
        console.log(`Registered ${card.agent_id}: ${resp.status}`);
      }
    }

    main().catch(console.error);
    ```
  </Tab>
</Tabs>

## Step 2 — Discover agents by skill [#step-2--discover-agents-by-skill]

The router doesn't hardcode `agent_id`s — it asks the registry which agents have a needed
skill. `GET /agents?skill_tags=...` returns only agents whose skills carry **all** of the
requested tags (comma-separated). Filtering `nlp` returns both the translate and summarize
agents; filtering `echo` returns just one.

<Tabs groupId="language" items="['curl','C#','Go','Java','Python','TypeScript']">
  <Tab value="curl">
    ```bash
    # Agents that can do NLP work
    curl "http://localhost:9090/agents?skill_tags=nlp"

    # Agents that can echo
    curl "http://localhost:9090/agents?skill_tags=echo"
    ```
  </Tab>

  <Tab value="C#">
    ```csharp
    using System.Text.Json.Nodes;

    const string KubeMqUrl = "http://localhost:9090";
    using var client = new HttpClient();

    async Task<List<string>> Discover(string tag)
    {
        var resp = await client.GetAsync($"{KubeMqUrl}/agents?skill_tags={tag}");
        var root = JsonNode.Parse(await resp.Content.ReadAsStringAsync())!;
        var agents = root is JsonArray arr ? arr : root["agents"]!.AsArray();
        return agents.Select(a => a!["agent_id"]!.GetValue<string>()).ToList();
    }

    Console.WriteLine($"nlp:  [{string.Join(", ", await Discover("nlp"))}]");
    Console.WriteLine($"echo: [{string.Join(", ", await Discover("echo"))}]");
    ```
  </Tab>

  <Tab value="Go">
    ```go
    package main

    import (
    	"encoding/json"
    	"fmt"
    	"io"
    	"net/http"
    )

    const kubemqURL = "http://localhost:9090"

    func discover(tag string) []string {
    	resp, err := http.Get(kubemqURL + "/agents?skill_tags=" + tag)
    	if err != nil {
    		return nil
    	}
    	defer resp.Body.Close()
    	body, _ := io.ReadAll(resp.Body)

    	var wrapper map[string]interface{}
    	json.Unmarshal(body, &wrapper)
    	agents, _ := wrapper["agents"].([]interface{})
    	ids := make([]string, 0, len(agents))
    	for _, a := range agents {
    		ids = append(ids, a.(map[string]interface{})["agent_id"].(string))
    	}
    	return ids
    }

    func main() {
    	fmt.Printf("nlp:  %v\n", discover("nlp"))
    	fmt.Printf("echo: %v\n", discover("echo"))
    }
    ```
  </Tab>

  <Tab value="Java">
    ```java
    import com.fasterxml.jackson.databind.ObjectMapper;

    import java.net.URI;
    import java.net.http.HttpClient;
    import java.net.http.HttpRequest;
    import java.net.http.HttpResponse;
    import java.util.ArrayList;
    import java.util.List;

    public class Client {

        static final String KUBEMQ_URL = "http://localhost:9090";
        static final ObjectMapper MAPPER = new ObjectMapper();
        static final HttpClient CLIENT = HttpClient.newHttpClient();

        static List<String> discover(String tag) throws Exception {
            var req = HttpRequest.newBuilder()
                .uri(URI.create(KUBEMQ_URL + "/agents?skill_tags=" + tag))
                .GET().build();
            var resp = CLIENT.send(req, HttpResponse.BodyHandlers.ofString());
            var root = MAPPER.readTree(resp.body());
            var agents = root.isArray() ? root : root.get("agents");
            var ids = new ArrayList<String>();
            for (var agent : agents) ids.add(agent.get("agent_id").asText());
            return ids;
        }

        public static void main(String[] args) throws Exception {
            System.out.println("nlp:  " + discover("nlp"));
            System.out.println("echo: " + discover("echo"));
        }
    }
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import asyncio

    import httpx

    KUBEMQ_URL = "http://localhost:9090"


    async def discover(client: httpx.AsyncClient, tag: str) -> list[str]:
        resp = await client.get(f"{KUBEMQ_URL}/agents", params={"skill_tags": tag})
        data = resp.json()
        agents = data.get("agents", data) if isinstance(data, dict) else data
        return [a["agent_id"] for a in agents]


    async def main() -> None:
        async with httpx.AsyncClient() as client:
            print(f"nlp:  {await discover(client, 'nlp')}")
            print(f"echo: {await discover(client, 'echo')}")


    if __name__ == "__main__":
        asyncio.run(main())
    ```
  </Tab>

  <Tab value="TypeScript">
    ```typescript
    const KUBEMQ_URL = "http://localhost:9090";

    async function discover(tag: string): Promise<string[]> {
      const resp = await fetch(`${KUBEMQ_URL}/agents?skill_tags=${tag}`);
      const data = await resp.json();
      const agents = Array.isArray(data) ? data : (data.agents || []);
      return agents.map((a: { agent_id: string }) => a.agent_id);
    }

    async function main() {
      console.log("nlp: ", await discover("nlp"));
      console.log("echo:", await discover("echo"));
    }

    main().catch(console.error);
    ```
  </Tab>
</Tabs>

## Step 3 — Route a request by agent\_id [#step-3--route-a-request-by-agent_id]

Once the router has picked an agent, it sends a normal
[`message/send`](/aiway/a2a/sync-messaging) to `POST /a2a/{agent_id}`. The same
client can fan a workload across agents by choosing a different `agent_id` per call — the
gateway routes each request to the matching agent's virtual subscriber.

<Tabs groupId="language" items="['curl','C#','Go','Java','Python','TypeScript']">
  <Tab value="curl">
    ```bash
    # Route to the translate agent
    curl -X POST http://localhost:9090/a2a/translate-agent-01 \
      -H "Content-Type: application/json" \
      -d '{
        "jsonrpc": "2.0",
        "id": 1,
        "method": "message/send",
        "params": {"message": {"parts": [{"text": "Translate: hello"}]}}
      }'

    # Route to the summarize agent
    curl -X POST http://localhost:9090/a2a/summarize-agent-01 \
      -H "Content-Type: application/json" \
      -d '{
        "jsonrpc": "2.0",
        "id": 2,
        "method": "message/send",
        "params": {"message": {"parts": [{"text": "Summarize this report..."}]}}
      }'
    ```
  </Tab>

  <Tab value="C#">
    ```csharp
    using System.Text;
    using System.Text.Json.Nodes;

    const string KubeMqUrl = "http://localhost:9090";
    using var client = new HttpClient();

    async Task RouteTo(string agentId, string text)
    {
        var payload = new JsonObject
        {
            ["jsonrpc"] = "2.0",
            ["id"] = 1,
            ["method"] = "message/send",
            ["params"] = new JsonObject
            {
                ["message"] = new JsonObject
                {
                    ["parts"] = new JsonArray(new JsonObject { ["text"] = text })
                }
            }
        };
        var resp = await client.PostAsync(
            $"{KubeMqUrl}/a2a/{agentId}",
            new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json"));
        Console.WriteLine($"{agentId} -> {(int)resp.StatusCode}");
    }

    await RouteTo("translate-agent-01", "Translate: hello");
    await RouteTo("summarize-agent-01", "Summarize this report...");
    ```
  </Tab>

  <Tab value="Go">
    ```go
    package main

    import (
    	"bytes"
    	"encoding/json"
    	"fmt"
    	"net/http"
    )

    const kubemqURL = "http://localhost:9090"

    func routeTo(agentID, text string) {
    	payload := map[string]interface{}{
    		"jsonrpc": "2.0", "id": 1, "method": "message/send",
    		"params": map[string]interface{}{
    			"message": map[string]interface{}{
    				"parts": []map[string]interface{}{{"text": text}},
    			},
    		},
    	}
    	data, _ := json.Marshal(payload)
    	resp, err := http.Post(kubemqURL+"/a2a/"+agentID, "application/json", bytes.NewReader(data))
    	if err != nil {
    		fmt.Printf("%s -> error: %v\n", agentID, err)
    		return
    	}
    	defer resp.Body.Close()
    	fmt.Printf("%s -> %d\n", agentID, resp.StatusCode)
    }

    func main() {
    	routeTo("translate-agent-01", "Translate: hello")
    	routeTo("summarize-agent-01", "Summarize this report...")
    }
    ```
  </Tab>

  <Tab value="Java">
    ```java
    import com.fasterxml.jackson.databind.ObjectMapper;

    import java.net.URI;
    import java.net.http.HttpClient;
    import java.net.http.HttpRequest;
    import java.net.http.HttpResponse;
    import java.util.List;
    import java.util.Map;

    public class Client {

        static final String KUBEMQ_URL = "http://localhost:9090";
        static final ObjectMapper MAPPER = new ObjectMapper();
        static final HttpClient CLIENT = HttpClient.newHttpClient();

        static void routeTo(String agentId, String text) throws Exception {
            var payload = Map.of(
                "jsonrpc", "2.0", "id", 1, "method", "message/send",
                "params", Map.of("message", Map.of("parts", List.of(Map.of("text", text))))
            );
            var req = HttpRequest.newBuilder()
                .uri(URI.create(KUBEMQ_URL + "/a2a/" + agentId))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(payload)))
                .build();
            var resp = CLIENT.send(req, HttpResponse.BodyHandlers.ofString());
            System.out.println(agentId + " -> " + resp.statusCode());
        }

        public static void main(String[] args) throws Exception {
            routeTo("translate-agent-01", "Translate: hello");
            routeTo("summarize-agent-01", "Summarize this report...");
        }
    }
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import asyncio

    import httpx

    KUBEMQ_URL = "http://localhost:9090"


    async def route_to(client: httpx.AsyncClient, agent_id: str, text: str) -> None:
        payload = {
            "jsonrpc": "2.0",
            "id": 1,
            "method": "message/send",
            "params": {"message": {"parts": [{"text": text}]}},
        }
        resp = await client.post(f"{KUBEMQ_URL}/a2a/{agent_id}", json=payload)
        print(f"{agent_id} -> {resp.status_code}")


    async def main() -> None:
        async with httpx.AsyncClient() as client:
            await route_to(client, "translate-agent-01", "Translate: hello")
            await route_to(client, "summarize-agent-01", "Summarize this report...")


    if __name__ == "__main__":
        asyncio.run(main())
    ```
  </Tab>

  <Tab value="TypeScript">
    ```typescript
    const KUBEMQ_URL = "http://localhost:9090";

    async function routeTo(agentId: string, text: string) {
      const request = {
        jsonrpc: "2.0",
        id: 1,
        method: "message/send",
        params: { message: { parts: [{ text }] } },
      };
      const resp = await fetch(`${KUBEMQ_URL}/a2a/${agentId}`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(request),
      });
      console.log(`${agentId} -> ${resp.status}`);
    }

    async function main() {
      await routeTo("translate-agent-01", "Translate: hello");
      await routeTo("summarize-agent-01", "Summarize this report...");
    }

    main().catch(console.error);
    ```
  </Tab>
</Tabs>

## How routing works [#how-routing-works]

The gateway is a thin router, not a load balancer — `agent_id` selects exactly one agent.

* **Addressing.** The `{agent_id}` path segment names the target. The gateway validates it
  against the registry, then issues a Query on `_AGENTS_.agents/<agent_id>`, which only
  that agent's virtual subscriber consumes.
* **Discovery vs. routing.** Skill tags are a **discovery** convenience for picking an
  `agent_id`; they never auto-route. The caller (or its own routing logic) decides which
  `agent_id` to send to.
* **Isolation.** Each agent has its own [concurrency cap](/aiway/a2a/guides/concurrency)
  (`AgentMaxConcurrency`, default 100) and its own liveness/TTL — one busy or expired agent
  does not affect the others.
* **Scaling out.** Want two interchangeable translators? Register them under different
  `agent_id`s with the same `translate` tag, then let your router pick between the matches
  returned by `GET /agents?skill_tags=translate`.

<Callout type="info">
  Skill-tag filtering matches agents that carry **all** requested tags, and the filter runs
  in memory after the registry fetch. To group agents for discovery, give them a shared tag
  (like `nlp` above) in addition to their specific skill.
</Callout>

## Related [#related]

<Cards>
  <Card title="Agent registry" href="/aiway/a2a/registry" description="Register, list, heartbeat, and deregister agents — the source of truth for routing." />

  <Card title="Synchronous messaging" href="/aiway/a2a/sync-messaging" description="The message/send call each routed request uses." />

  <Card title="How it works" href="/aiway/a2a/architecture" description="Virtual subscribers and the _AGENTS_.agents internal channels behind routing." />

  <Card title="Streaming task pipeline" href="/aiway/a2a/scenarios/streaming-task-pipeline" description="Drive a long-running task across the gateway with message/stream." />
</Cards>
