# Multi-Agent Orchestration

Three A2A agents communicating via the protocol: an LLM-powered orchestrator discovers remote agents and delegates tasks to the most appropriate one.

[View source on GitHub](https://github.com/general-intelligence-systems/agent2agent/tree/main/examples/multi-agent)

## What you'll learn

- Agent-to-agent communication via `A2A::Client`
- LLM-powered routing (Brute) to select the right agent for a request
- Agent card discovery at runtime
- Multi-service Docker Compose setup

## Architecture

| Service | Port | Role |
|---|---|---|
| **greeter** | 9292 | LLM-powered greeting generator |
| **translator** | 9293 | LLM-powered language translator |
| **host** | 9294 | Orchestrator -- discovers agents, routes requests via LLM |

The **host** agent discovers the greeter and translator agent cards at startup, then uses an LLM to decide which agent should handle each incoming request. It delegates via `A2A::Client.send_message` and returns the remote agent's response.

## Prerequisites

Requires an LLM API key. Set one of:

- `ANTHROPIC_API_KEY`
- `OPENAI_API_KEY`
- `GEMINI_API_KEY`

## Step 1: Start all three services

```bash
git clone https://github.com/general-intelligence-systems/agent2agent.git
cd agent2agent/examples/multi-agent
ANTHROPIC_API_KEY=sk-... docker compose up -d --build
```

Replace `sk-...` with your actual API key. If using OpenAI or Gemini, substitute the appropriate env var:

```bash
OPENAI_API_KEY=sk-... docker compose up -d --build
```

Expected output:

```
[+] Building 18.5s (27/27) FINISHED
[+] Running 3/3
 ✔ Container multi-agent-greeter-1     Started
 ✔ Container multi-agent-translator-1  Started
 ✔ Container multi-agent-host-1        Started
```

## Step 2: Check the logs

```bash
docker compose logs
```

Expected output:

```
greeter-1     |   0.0s     info: main [pid=1] [2025-05-01 12:00:00 +0000]
greeter-1     |                | Greeter Agent starting on :9292...
translator-1  |   0.0s     info: main [pid=1] [2025-05-01 12:00:00 +0000]
translator-1  |                | Translator Agent starting on :9293...
host-1        |   0.0s     info: main [pid=1] [2025-05-01 12:00:00 +0000]
host-1        |                | Host Orchestrator starting on :9294...
host-1        |   0.0s     info: main [pid=1] [2025-05-01 12:00:00 +0000]
host-1        |                | Remote agents: greeter=http://greeter:9292, translator=http://translator:9293
```

All three services should be running. The host knows where to find the remote agents but discovers their cards lazily on first request.

## Step 3: Send a greeting request (routed to greeter)

```bash
curl -s -X POST http://localhost:9294/ \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{
    "message":{"messageId":"m1","role":"ROLE_USER","parts":[{"text":"Greet Alice for her birthday"}]}
  }}' | jq .
```

Expected output:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "task": {
      "id": "be85b851-1234-5678-9abc-def012345678",
      "contextId": "a1b2c3d4-5678-9abc-def0-123456789abc",
      "status": {
        "state": "TASK_STATE_COMPLETED",
        "timestamp": "2025-05-01T12:00:02.345Z"
      },
      "artifacts": [
        {
          "artifactId": "...",
          "name": "delegated-response",
          "description": "Response from greeter agent",
          "parts": [
            {
              "text": "Happy Birthday, Alice! May your special day be filled with joy, laughter, and all the wonderful things you deserve. Here's to another year of amazing adventures!"
            }
          ],
          "metadata": {
            "delegatedTo": "greeter",
            "remoteTaskId": "..."
          }
        }
      ],
      "history": [
        {
          "messageId": "...",
          "role": "ROLE_AGENT",
          "parts": [
            {
              "text": "[Delegated to greeter] Happy Birthday, Alice! May your special day be filled with joy, laughter, and all the wonderful things you deserve. Here's to another year of amazing adventures!"
            }
          ]
        }
      ]
    }
  }
}
```

The key things to notice:
- The host routed this to the **greeter** agent (see `metadata.delegatedTo`).
- The greeting is generated by the LLM, so the exact text will vary.
- The history shows the delegation with `[Delegated to greeter]` prefix.

Check the host logs to see the routing decision:

```bash
docker compose logs host
```

You should see:

```
host-1  |   1.0s     info: main [pid=1] [2025-05-01 12:00:01 +0000]
host-1  |                | Discovered agent: Greeter Agent at http://greeter:9292
host-1  |   1.0s     info: main [pid=1] [2025-05-01 12:00:01 +0000]
host-1  |                | Discovered agent: Translator Agent at http://translator:9293
host-1  |   1.5s     info: main [pid=1] [2025-05-01 12:00:01 +0000]
host-1  |                | Routing to 'greeter' for: Greet Alice for her birthday
```

## Step 4: Send a translation request (routed to translator)

```bash
curl -s -X POST http://localhost:9294/ \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"SendMessage","params":{
    "message":{"messageId":"m2","role":"ROLE_USER","parts":[{"text":"Translate hello to Japanese"}]}
  }}' | jq .
```

Expected output:

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "task": {
      "id": "f6a7b8c9-0123-4567-89ab-cdef01234567",
      "contextId": "d5e6f7a8-9012-3456-789a-bcdef0123456",
      "status": {
        "state": "TASK_STATE_COMPLETED",
        "timestamp": "2025-05-01T12:00:05.678Z"
      },
      "artifacts": [
        {
          "artifactId": "...",
          "name": "delegated-response",
          "description": "Response from translator agent",
          "parts": [
            {
              "text": "こんにちは (Konnichiwa)"
            }
          ],
          "metadata": {
            "delegatedTo": "translator",
            "remoteTaskId": "..."
          }
        }
      ],
      "history": [
        {
          "messageId": "...",
          "role": "ROLE_AGENT",
          "parts": [
            {
              "text": "[Delegated to translator] こんにちは (Konnichiwa)"
            }
          ]
        }
      ]
    }
  }
}
```

This time the host routed to the **translator** agent. The LLM generates the actual translation, so the exact output will vary.

## Step 5: Cleanup

```bash
docker compose down
```

## Files

| File | Purpose |
|---|---|
| `greeter/config.ru` | Greeter agent -- generates creative greetings via Brute |
| `translator/config.ru` | Translator agent -- translates text via Brute |
| `host/config.ru` | Host orchestrator -- discovers agents, routes via LLM |
| `*/falcon.rb` | Falcon server configs for each service |
| `*/Gemfile` | Per-service dependencies |
| `*/Dockerfile` | Per-service container build |
| `docker-compose.yml` | Three-service compose config |

[View source on GitHub](https://github.com/general-intelligence-systems/agent2agent/tree/main/examples/multi-agent)
