KubeMQ
AiwayAI Agents (A2A)

Synchronous Messaging

Send JSON-RPC 2.0 message/send requests to A2A agents — context IDs, custom methods, header forwarding, and concurrent calls.

Synchronous A2A messaging is a request/reply call: a caller POSTs a JSON-RPC 2.0 request to POST /a2a/<agent_id>, the gateway routes it to the target agent through its virtual subscriber, and the agent's reply comes back on the same HTTP response — no SSE, no polling.

Overview

message/send is the workhorse of the A2A connector. You address an agent by ID in the URL, put a standard JSON-RPC 2.0 envelope in the body, and read the reply synchronously. The gateway is method-agnostic: it forwards whatever method you send — message/send, tasks/get, tasks/cancel, or your own custom/action — and returns the agent's response as-is. Use synchronous messaging whenever you want a single answer back in one round-trip; switch to streaming when the agent produces incremental output over time.

How it works

A synchronous call travels from the caller through the gateway and the agent's virtual subscriber to the plain HTTP agent, then the reply retraces the path.

The gateway proxies one request to one agent and relays the reply on the same HTTP response.

The gateway validates the agent ID and Content-Type, reads the timeout from params.configuration.timeout (falling back to DefaultTimeoutSeconds, capped at MaxTimeoutSeconds), packs the caller's X-* headers, and issues a Query on _AGENTS_.agents/<agent_id>. The agent's virtual subscriber unpacks the request, POSTs it to the agent's registered URL, and relays the response.

Basic message/send

Send a text message and read the synchronous reply. The agent receives the full JSON-RPC envelope and returns its result.

curl -X POST http://localhost:9090/a2a/echo-agent-01 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "message/send",
    "params": {
      "message": {
        "parts": [{"text": "Hello, agent!"}]
      }
    }
  }'
const string KubeMqUrl = "http://localhost:9090";
const string AgentId = "echo-agent-01";

var payload = new JsonObject
{
    ["jsonrpc"] = "2.0",
    ["id"] = 1,
    ["method"] = "message/send",
    ["params"] = new JsonObject
    {
        ["message"] = new JsonObject
        {
            ["parts"] = new JsonArray(new JsonObject { ["text"] = "Hello, agent!" })
        }
    }
};

using var client = new HttpClient();
var resp = await client.PostAsync(
    $"{KubeMqUrl}/a2a/{AgentId}",
    new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json"));

Console.WriteLine($"Status: {(int)resp.StatusCode}");
var body = await resp.Content.ReadAsStringAsync();
var data = JsonNode.Parse(body)!;
Console.WriteLine(JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }));

if (data["result"] != null)
    Console.WriteLine("\nBasic send completed successfully!");
const (
    kubemqURL = "http://localhost:9090"
    agentID   = "echo-agent-01"
)

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": "Hello, agent!"}},
        },
    },
}

data, _ := json.Marshal(payload)
resp, err := http.Post(kubemqURL+"/a2a/"+agentID, "application/json", bytes.NewReader(data))
if err != nil {
    fmt.Fprintf(os.Stderr, "Request failed: %v\n", err)
    os.Exit(1)
}
defer resp.Body.Close()

body, _ := io.ReadAll(resp.Body)
fmt.Printf("Status: %d\n", resp.StatusCode)
fmt.Println(string(body))
static final String KUBEMQ_URL = "http://localhost:9090";
static final String AGENT_ID = "echo-agent-01";
static final ObjectMapper MAPPER = new ObjectMapper();

var payload = Map.of(
    "jsonrpc", "2.0",
    "id", 1,
    "method", "message/send",
    "params", Map.of(
        "message", Map.of(
            "parts", List.of(Map.of("text", "Hello, agent!"))
        )
    )
);

var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder()
    .uri(URI.create(KUBEMQ_URL + "/a2a/" + AGENT_ID))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(payload)))
    .build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + resp.statusCode());

var data = MAPPER.readTree(resp.body());
System.out.println(MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(data));
import httpx

KUBEMQ_URL = "http://localhost:9090"
AGENT_ID = "echo-agent-01"

payload = {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "message/send",
    "params": {
        "message": {
            "parts": [{"text": "Hello, agent!"}],
        },
    },
}

async with httpx.AsyncClient() as client:
    resp = await client.post(f"{KUBEMQ_URL}/a2a/{AGENT_ID}", json=payload)
    print(f"Status: {resp.status_code}")
    data = resp.json()
    print(json.dumps(data, indent=2))
    assert "result" in data
const KUBEMQ_URL = "http://localhost:9090";
const AGENT_ID = "echo-agent-01";

const request = {
  jsonrpc: "2.0",
  id: 1,
  method: "message/send",
  params: {
    message: {
      parts: [{ text: "Hello, agent!" }],
    },
  },
};

const resp = await fetch(`${KUBEMQ_URL}/a2a/${AGENT_ID}`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(request),
});

const data = await resp.json();
console.log("Response:", JSON.stringify(data, null, 2));

Context IDs

Set params.contextId to correlate a sequence of messages with the same agent — for conversation threading or session management. The gateway forwards it to the agent, which echoes it back so callers can confirm the correlation.

curl -X POST http://localhost:9090/a2a/context-agent-01 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "message/send",
    "params": {
      "message": {"parts": [{"text": "Track this request"}]},
      "contextId": "ctx-001"
    }
  }'
var payload = new JsonObject
{
    ["jsonrpc"] = "2.0",
    ["id"] = 1,
    ["method"] = "message/send",
    ["params"] = new JsonObject
    {
        ["message"] = new JsonObject
        {
            ["parts"] = new JsonArray(new JsonObject { ["text"] = "Track this request" })
        },
        ["contextId"] = "ctx-001"
    }
};

using var client = new HttpClient();
var resp = await client.PostAsync(
    $"{KubeMqUrl}/a2a/context-agent-01",
    new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json"));

var body = await resp.Content.ReadAsStringAsync();
var data = JsonNode.Parse(body)!;
var returnedCtx = data["result"]?["contextId"]?.GetValue<string>();
Console.WriteLine($"Received contextId: {returnedCtx}");
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": "Track this request"}}},
        "contextId": "ctx-001",
    },
}

data, _ := json.Marshal(payload)
resp, _ := http.Post("http://localhost:9090/a2a/context-agent-01", "application/json", bytes.NewReader(data))
defer resp.Body.Close()

body, _ := io.ReadAll(resp.Body)
var result map[string]interface{}
json.Unmarshal(body, &result)
if r, ok := result["result"].(map[string]interface{}); ok {
    fmt.Printf("Received contextId: %v\n", r["contextId"])
}
var payload = Map.of(
    "jsonrpc", "2.0",
    "id", 1,
    "method", "message/send",
    "params", Map.of(
        "message", Map.of("parts", List.of(Map.of("text", "Track this request"))),
        "contextId", "ctx-001"
    )
);

var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder()
    .uri(URI.create(KUBEMQ_URL + "/a2a/context-agent-01"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(payload)))
    .build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());

var data = MAPPER.readTree(resp.body());
var returnedCtx = data.path("result").path("contextId").asText(null);
System.out.println("Received contextId: " + returnedCtx);
payload = {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "message/send",
    "params": {
        "message": {"parts": [{"text": "Track this request"}]},
        "contextId": "ctx-001",
    },
}

async with httpx.AsyncClient() as client:
    resp = await client.post("http://localhost:9090/a2a/context-agent-01", json=payload)
    data = resp.json()
    returned_ctx = data.get("result", {}).get("contextId")
    print(f"Received contextId: {returned_ctx}")
const contextId = "ctx-001";

const request = {
  jsonrpc: "2.0",
  id: 1,
  method: "message/send",
  params: {
    message: { parts: [{ text: "Track this request" }] },
    contextId,
  },
};

const resp = await fetch(`${KUBEMQ_URL}/a2a/context-agent-01`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(request),
});

const data = await resp.json();
const echoedCtx = data.result?.contextId;
console.log(`Echoed contextId: ${echoedCtx}`);

Custom methods

The gateway forwards any JSON-RPC method name to the agent unchanged — standard A2A methods like tasks/get and tasks/cancel, or your own custom/action. KubeMQ does not interpret the method; the agent decides how to handle it.

curl -X POST http://localhost:9090/a2a/custom-method-agent-01 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "custom/action",
    "params": {"data": "custom-payload"}
  }'
var payload = new JsonObject
{
    ["jsonrpc"] = "2.0",
    ["id"] = 1,
    ["method"] = "custom/action",
    ["params"] = new JsonObject { ["data"] = "custom-payload" }
};

using var client = new HttpClient();
var resp = await client.PostAsync(
    $"{KubeMqUrl}/a2a/custom-method-agent-01",
    new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json"));

var body = await resp.Content.ReadAsStringAsync();
var data = JsonNode.Parse(body)!;
var handledMethod = data["result"]?["handled_method"]?.GetValue<string>();
Console.WriteLine($"Handled method: {handledMethod}");
payload := map[string]interface{}{
    "jsonrpc": "2.0",
    "id":      1,
    "method":  "custom/action",
    "params":  map[string]interface{}{"data": "custom-payload"},
}

data, _ := json.Marshal(payload)
resp, _ := http.Post("http://localhost:9090/a2a/custom-method-agent-01", "application/json", bytes.NewReader(data))
defer resp.Body.Close()

body, _ := io.ReadAll(resp.Body)
var result map[string]interface{}
json.Unmarshal(body, &result)
r, _ := result["result"].(map[string]interface{})
fmt.Printf("Handled method: %v\n", r["handled_method"])
var payload = Map.of(
    "jsonrpc", "2.0",
    "id", 1,
    "method", "custom/action",
    "params", Map.of("data", "custom-payload")
);

var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder()
    .uri(URI.create(KUBEMQ_URL + "/a2a/custom-method-agent-01"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(payload)))
    .build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());

var data = MAPPER.readTree(resp.body());
System.out.println("Handled method: " + data.path("result").path("handled_method").asText());
payload = {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "custom/action",
    "params": {"data": "custom-payload"},
}

async with httpx.AsyncClient() as client:
    resp = await client.post("http://localhost:9090/a2a/custom-method-agent-01", json=payload)
    data = resp.json()
    result = data.get("result", {})
    print(f"Handled method: {result.get('handled_method')}")
async function sendRpc(method: string, params: unknown) {
  const request = { jsonrpc: "2.0", id: Date.now(), method, params };
  const resp = await fetch(`${KUBEMQ_URL}/a2a/custom-method-agent-01`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(request),
  });
  return resp.json();
}

const custom = await sendRpc("custom/action", { data: "custom-payload" });
console.log("Response:", JSON.stringify(custom, null, 2));

Header forwarding

Caller request headers prefixed with X- are forwarded to the agent; hop-by-hop and sensitive headers (Authorization, Cookie, X-Forwarded-For, and others) are stripped. The gateway always injects X-KubeMQ-Caller-ID so the agent knows which client originated the call.

curl -X POST http://localhost:9090/a2a/header-agent-01 \
  -H "Content-Type: application/json" \
  -H "X-Custom-Header: my-custom-value" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "message/send",
    "params": {
      "message": {"parts": [{"text": "Check my headers"}]}
    }
  }'
var payload = new JsonObject
{
    ["jsonrpc"] = "2.0",
    ["id"] = 1,
    ["method"] = "message/send",
    ["params"] = new JsonObject
    {
        ["message"] = new JsonObject
        {
            ["parts"] = new JsonArray(new JsonObject { ["text"] = "Check my headers" })
        }
    }
};

using var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, $"{KubeMqUrl}/a2a/header-agent-01")
{
    Content = new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json")
};
request.Headers.Add("X-Custom-Header", "my-custom-value");

var resp = await client.SendAsync(request);
var body = await resp.Content.ReadAsStringAsync();
var data = JsonNode.Parse(body)!;
var received = data["result"]?["received_headers"];
Console.WriteLine($"Forwarded headers: {received?.ToJsonString()}");
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": "Check my headers"}},
        },
    },
}

data, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "http://localhost:9090/a2a/header-agent-01", bytes.NewReader(data))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Custom-Header", "my-custom-value")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()

body, _ := io.ReadAll(resp.Body)
var result map[string]interface{}
json.Unmarshal(body, &result)
received, _ := result["result"].(map[string]interface{})["received_headers"].(map[string]interface{})
fmt.Printf("Forwarded headers: %v\n", received)
var payload = Map.of(
    "jsonrpc", "2.0",
    "id", 1,
    "method", "message/send",
    "params", Map.of(
        "message", Map.of("parts", List.of(Map.of("text", "Check my headers")))
    )
);

var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder()
    .uri(URI.create(KUBEMQ_URL + "/a2a/header-agent-01"))
    .header("Content-Type", "application/json")
    .header("X-Custom-Header", "my-custom-value")
    .POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(payload)))
    .build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());

var data = MAPPER.readTree(resp.body());
System.out.println("Forwarded headers: " + data.path("result").path("received_headers"));
payload = {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "message/send",
    "params": {
        "message": {"parts": [{"text": "Check my headers"}]},
    },
}

async with httpx.AsyncClient() as client:
    resp = await client.post(
        "http://localhost:9090/a2a/header-agent-01",
        json=payload,
        headers={"X-Custom-Header": "my-custom-value"},
    )
    data = resp.json()
    received = data.get("result", {}).get("received_headers", {})
    print(f"Forwarded headers: {received}")
const request = {
  jsonrpc: "2.0",
  id: 1,
  method: "message/send",
  params: {
    message: { parts: [{ text: "Check my headers" }] },
  },
};

const resp = await fetch(`${KUBEMQ_URL}/a2a/header-agent-01`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Custom-Header": "test-value-123",
  },
  body: JSON.stringify(request),
});

const data = await resp.json();
const headers = data.result?.received_headers || {};
console.log(`X-Custom-Header:    ${headers["x-custom-header"] || "(not forwarded)"}`);
console.log(`X-KubeMQ-Caller-ID: ${headers["x-kubemq-caller-id"] || "(not present)"}`);

Concurrent requests

Multiple simultaneous calls to the same agent are processed in parallel up to the per-agent concurrency cap (AgentMaxConcurrency, default 100). Requests beyond the cap are rejected immediately with a "server busy" error instead of being queued — see Concurrency & limits.

# Fire 20 requests in parallel against the same agent
for i in $(seq 1 20); do
  curl -s -X POST http://localhost:9090/a2a/concurrent-agent-01 \
    -H "Content-Type: application/json" \
    -d "{\"jsonrpc\":\"2.0\",\"id\":$i,\"method\":\"message/send\",\"params\":{\"message\":{\"parts\":[{\"text\":\"Request #$i\"}]}}}" &
done
wait
const int NumRequests = 20;

async Task<JsonNode?> SendRequest(HttpClient httpClient, int requestId)
{
    var payload = new JsonObject
    {
        ["jsonrpc"] = "2.0",
        ["id"] = requestId,
        ["method"] = "message/send",
        ["params"] = new JsonObject
        {
            ["message"] = new JsonObject
            {
                ["parts"] = new JsonArray(new JsonObject { ["text"] = $"Request #{requestId}" })
            }
        }
    };
    var resp = await httpClient.PostAsync(
        $"{KubeMqUrl}/a2a/concurrent-agent-01",
        new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json"));
    var body = await resp.Content.ReadAsStringAsync();
    return JsonNode.Parse(body);
}

using var client = new HttpClient();
var tasks = Enumerable.Range(1, NumRequests).Select(i => SendRequest(client, i)).ToArray();
var results = await Task.WhenAll(tasks);

var successes = results.Count(r => r?["result"] != null);
Console.WriteLine($"Successes: {successes} / {NumRequests}");
const numRequests = 20

results := make(chan map[string]interface{}, numRequests)
var wg sync.WaitGroup

for i := 1; i <= numRequests; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        payload := map[string]interface{}{
            "jsonrpc": "2.0", "id": id, "method": "message/send",
            "params": map[string]interface{}{
                "message": map[string]interface{}{
                    "parts": []map[string]interface{}{{"text": fmt.Sprintf("Request #%d", id)}},
                },
            },
        }
        data, _ := json.Marshal(payload)
        resp, err := http.Post("http://localhost:9090/a2a/concurrent-agent-01", "application/json", bytes.NewReader(data))
        if err != nil {
            results <- nil
            return
        }
        defer resp.Body.Close()
        body, _ := io.ReadAll(resp.Body)
        var r map[string]interface{}
        json.Unmarshal(body, &r)
        results <- r
    }(i)
}
wg.Wait()
close(results)

successes := 0
for r := range results {
    if r != nil {
        if _, ok := r["result"]; ok {
            successes++
        }
    }
}
fmt.Printf("Successes: %d / %d\n", successes, numRequests)
static final int NUM_REQUESTS = 20;

var client = HttpClient.newHttpClient();
List<CompletableFuture<HttpResponse<String>>> futures = new ArrayList<>();
for (int i = 1; i <= NUM_REQUESTS; i++) {
    var payload = Map.of(
        "jsonrpc", "2.0", "id", i, "method", "message/send",
        "params", Map.of(
            "message", Map.of("parts", List.of(Map.of("text", "Request #" + i)))
        )
    );
    var req = HttpRequest.newBuilder()
        .uri(URI.create(KUBEMQ_URL + "/a2a/concurrent-agent-01"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(payload)))
        .build();
    futures.add(client.sendAsync(req, HttpResponse.BodyHandlers.ofString()));
}

CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

int successes = 0;
for (var future : futures) {
    var data = MAPPER.readTree(future.get().body());
    if (data.has("result")) successes++;
}
System.out.println("Successes: " + successes + " / " + NUM_REQUESTS);
NUM_REQUESTS = 20

async def send_request(client: httpx.AsyncClient, request_id: int) -> dict:
    payload = {
        "jsonrpc": "2.0",
        "id": request_id,
        "method": "message/send",
        "params": {
            "message": {"parts": [{"text": f"Request #{request_id}"}]},
        },
    }
    resp = await client.post("http://localhost:9090/a2a/concurrent-agent-01", json=payload)
    return resp.json()

async with httpx.AsyncClient() as client:
    tasks = [send_request(client, i) for i in range(1, NUM_REQUESTS + 1)]
    results = await asyncio.gather(*tasks, return_exceptions=True)

successes = sum(1 for r in results if isinstance(r, dict) and "result" in r)
print(f"Successes: {successes} / {NUM_REQUESTS}")
const NUM_REQUESTS = 20;

async function sendRequest(id: number): Promise<boolean> {
  const request = {
    jsonrpc: "2.0",
    id,
    method: "message/send",
    params: { message: { parts: [{ text: `Request ${id}` }] } },
  };
  try {
    const resp = await fetch(`${KUBEMQ_URL}/a2a/concurrent-agent-01`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(request),
    });
    const data = await resp.json();
    return !!data.result;
  } catch {
    return false;
  }
}

const promises = Array.from({ length: NUM_REQUESTS }, (_, i) => sendRequest(i + 1));
const results = await Promise.all(promises);
const succeeded = results.filter((r) => r).length;
console.log(`Succeeded: ${succeeded} / ${NUM_REQUESTS}`);

Parameters

The body is a standard JSON-RPC 2.0 request. Method-specific fields live under params.

FieldTypeRequiredDefaultDescription
jsonrpcstringyesMust be "2.0".
idstring | numberyesRequest identifier; echoed back on the response.
methodstringyesAny JSON-RPC method name, e.g. message/send, tasks/get, or a custom one. Empty method is rejected with -32600.
params.message.partsarrayfor message/sendMessage content parts, each with a text (or other typed) field.
params.contextIdstringnoCorrelation ID forwarded to the agent for conversation threading.
params.configuration.timeoutnumbernoDefaultTimeoutSeconds (300)Per-request timeout in seconds, capped at MaxTimeoutSeconds (3600).

The URL path segment <agent_id> selects the target agent and is validated against the registry before routing.

Response

On success the gateway returns the agent's JSON-RPC result verbatim (the echo agent mirrors the request under result.echo):

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "echo": {
      "jsonrpc": "2.0",
      "id": 1,
      "method": "message/send",
      "params": {
        "message": { "parts": [{ "text": "Hello, agent!" }] }
      }
    }
  }
}

A response carrying an error object signals a problem. KubeMQ distinguishes two failure classes:

  • Transport errors — the agent never processed the request (unreachable, timeout, 502/503/504, or an oversized response). Internally these surface as Executed: false; safe to retry.
  • Application errors — the agent processed the request and returned a JSON-RPC error in its response body (Executed: true). Retrying without changing the request usually will not help.

A timeout, for example, returns:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32001,
    "message": "agent timeout: echo-agent-01"
  }
}

See Error handling for the full code list and retry guidance.

Was this page helpful?

On this page