KubeMQ
AiwayTutorial

3. Discover & invoke

Find your agent by capability and call it synchronously with message/send.

In Step 2 you registered the research-agent-01 agent with a research skill. Now you'll act as a caller: first discover the agent by its capability (no need to know its ID up front), then invoke it synchronously and read the result back in a single round-trip.

This step assumes KubeMQ is running on port 9090 and research-agent-01 is already registered (Steps 1 and 2). Verify with curl http://localhost:9090/agents — the agent should appear in the roster.

Discover by capability

Callers don't have to hard-code agent IDs. The registry lets you find agents by skill tag: GET /agents?skill_tags=research returns every registered agent that advertises the research skill as a bare JSON array of agent cards. This is capability-based discovery — ask for what you need done, get back who can do it.

curl "http://localhost:9090/agents?skill_tags=research"
import asyncio

import httpx

KUBEMQ_URL = "http://localhost:9090"


async def main() -> None:
    async with httpx.AsyncClient() as client:
        resp = await client.get(
            f"{KUBEMQ_URL}/agents",
            params={"skill_tags": "research"},
        )
        agents = resp.json()
        for agent in agents:
            skills = [s["id"] for s in agent.get("skills", [])]
            print(f"  {agent['agent_id']}: skills={skills}, url={agent['url']}")


if __name__ == "__main__":
    asyncio.run(main())

The response includes research-agent-01 — its agent_id, url, and advertised skills:

[
  {
    "agent_id": "research-agent-01",
    "name": "Research Agent",
    "url": "http://localhost:18080/",
    "skills": [
      {
        "id": "research",
        "name": "Research",
        "description": "Searches sources and returns a summary.",
        "tags": ["research", "summarize"]
      }
    ],
    "protocolVersions": ["1.0"]
  }
]

skill_tags accepts a comma-separated list (?skill_tags=research,summarize) and is matched in memory after the roster is fetched. See the Agent registry for the full list/filter API.

Invoke synchronously

Once you know the agent's ID, invoke it by POSTing a JSON-RPC 2.0 message/send envelope to POST /a2a/research-agent-01. The gateway routes the request to the agent through its Agent Bridge (the per-agent virtual subscriber) and relays the agent's reply on the same HTTP response — no SSE, no polling.

curl -X POST http://localhost:9090/a2a/research-agent-01 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "message/send",
    "params": {
      "message": {
        "parts": [{"text": "Summarize the history of message queues"}]
      }
    }
  }'
import asyncio
import json

import httpx

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


async def main() -> None:
    payload = {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "message/send",
        "params": {
            "message": {
                "parts": [{"text": "Summarize the history of message queues"}],
            },
        },
    }

    async with httpx.AsyncClient() as client:
        resp = await client.post(f"{KUBEMQ_URL}/a2a/{AGENT_ID}", json=payload)
        data = resp.json()
        print(json.dumps(data, indent=2))

        if "error" in data:
            print(f"\nAgent returned an error: {data['error']['message']}")
        else:
            print("\nGot a synchronous result from the agent.")


if __name__ == "__main__":
    asyncio.run(main())

The agent's JSON-RPC result comes back verbatim under result:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "summary": "Research summary for: Summarize the history of message queues",
    "sources": 3
  }
}

Header forwarding & error handling

A couple of behaviors are worth knowing before you build real callers:

  • Header forwarding — any request header you send prefixed with X- is forwarded to the agent (hop-by-hop and sensitive headers like Authorization and Cookie are stripped), and the gateway always injects X-KubeMQ-Caller-ID so the agent knows who originated the call. Use this to pass tracing or tenant context through to your agent.

  • Transport vs application errors — when a response carries an error object, KubeMQ distinguishes two classes:

    • Transport errors — the agent never processed the request (unreachable, timeout, 502/503/504, or an oversized reply). Internally these surface as Executed: false, and they are safe to retry.
    • Application errors — the agent processed the request and chose to return a JSON-RPC error (Executed: true). Retrying the same request usually won't help — fix the request instead.

    Tell them apart so your retry logic only re-sends calls that never reached the agent.

For the full message/send envelope (context IDs, custom methods), header-forwarding rules, and the complete error-code list, see Synchronous messaging and the Agent registry.

Next step

You've discovered and invoked your agent in one round-trip. Next, switch from a single reply to a live stream of task events as the agent works.

Was this page helpful?

On this page