# 3. Discover & invoke (/aiway/tutorial/discover-and-invoke)



In [Step 2](/aiway/tutorial/build-and-register-an-agent) 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.

<Callout type="info">
  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.
</Callout>

<Steps>
  <Step>
    ## Discover by capability [#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*.

    <Tabs groupId="language" items="['curl','Python']">
      <Tab value="curl">
        ```bash
        curl "http://localhost:9090/agents?skill_tags=research"
        ```
      </Tab>

      <Tab value="Python">
        ```python
        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())
        ```
      </Tab>
    </Tabs>

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

    ```json
    [
      {
        "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"]
      }
    ]
    ```

    <Callout type="info">
      `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](/aiway/a2a/registry) for the full list/filter API.
    </Callout>
  </Step>

  <Step>
    ## Invoke synchronously [#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](/aiway/a2a/architecture) (the per-agent virtual subscriber) and
    relays the agent's reply on the **same HTTP response** — no SSE, no polling.

    <Tabs groupId="language" items="['curl','Python']">
      <Tab value="curl">
        ```bash
        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"}]
              }
            }
          }'
        ```
      </Tab>

      <Tab value="Python">
        ```python
        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())
        ```
      </Tab>
    </Tabs>

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

    ```json
    {
      "jsonrpc": "2.0",
      "id": 1,
      "result": {
        "summary": "Research summary for: Summarize the history of message queues",
        "sources": 3
      }
    }
    ```
  </Step>
</Steps>

## Header forwarding & error handling [#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.

<Callout type="info">
  For the full `message/send` envelope (context IDs, custom methods), header-forwarding
  rules, and the complete error-code list, see
  [Synchronous messaging](/aiway/a2a/sync-messaging) and the
  [Agent registry](/aiway/a2a/registry).
</Callout>

## Next step [#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.

<Cards>
  <Card title="4. Stream live results" href="/aiway/tutorial/stream-live-results" description="Use message/stream to receive task.status, task.artifact, and task.done events live over SSE." />
</Cards>
