> ## Documentation Index
> Fetch the complete documentation index at: https://developers.telnyx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# A2A Agents

> Let a Telnyx AI Assistant delegate to remote agents that speak the A2A (Agent2Agent) protocol. Register an agent once and its advertised skills become tools automatically.

The `a2a_agents` field lets an assistant hand a question to a remote agent that speaks the [A2A (Agent2Agent) protocol](https://a2a-protocol.org/). You register the agent's URL and credentials once; the skills it advertises on its agent card become tools the assistant can call.

This is the point of configuring *agents* rather than tools. When the remote agent gains, renames, or drops a skill, the assistant follows without a change to your assistant configuration — on the first conversation that starts after the five-minute [card cache](#how-it-works) expires. It is the same bargain MCP makes with `list_tools`.

<Warning>
  **Beta:** A2A agent support is in beta. The `a2a_agents` configuration described here may change before general availability.
</Warning>

In this guide, you will learn:

* How an agent card becomes tools
* How to configure an agent, authenticate to it, and pass secrets
* When to make a delegation asynchronous
* Which limits apply and what happens when an agent is unreachable

***

## How it works

1. **You configure an agent** — a name, a URL, and any headers it needs.
2. **The card is fetched at conversation start.** `/.well-known/agent-card.json` is appended to the URL's path, unless that path already ends in `.json`, in which case the URL is used as-is.
3. **Each advertised skill becomes one tool**, named `a2a_<name>_<skill_id>`, described by the skill's own description from the card.
4. **The model calls a tool** with a single free-text `message`. A2A agents route on the message itself, so there is no per-skill parameter schema to mirror.
5. **The agent's reply comes back as the tool result**, and the assistant speaks it.

Cards are cached for five minutes, so only the first conversation to use an agent pays the round trip.

***

## Configure an agent

Add `a2a_agents` when you create or update an assistant:

```json theme={null}
{
  "name": "Support assistant",
  "model": "meta-llama/Meta-Llama-3.1-70B-Instruct",
  "instructions": "You are a support assistant. Use the billing agent for anything about invoices or payments.",
  "a2a_agents": [
    {
      "name": "billing_agent",
      "url": "https://agents.example.com/billing",
      "headers": [
        {
          "name": "Authorization",
          "value": "Bearer {{#integration_secret}}billing_agent_token{{/integration_secret}}"
        }
      ],
      "timeout_ms": 30000
    }
  ]
}
```

With that configuration, the card at `https://agents.example.com/billing/.well-known/agent-card.json` is fetched at conversation start. A card advertising skills `refund_status` and `invoice_lookup` gives the assistant two tools: `a2a_billing_agent_refund_status` and `a2a_billing_agent_invoice_lookup`.

### Fields

| Field              | Required | Description                                                                                                   |
| ------------------ | -------- | ------------------------------------------------------------------------------------------------------------- |
| `name`             | Yes      | Identifies the agent and seeds its derived tool names. At most 43 characters.                                 |
| `url`              | Yes      | The agent's base URL, or its agent-card URL directly. At most 2,048 bytes.                                    |
| `headers`          | No       | Sent both when fetching the card and on every call to the agent.                                              |
| `async`            | No       | Defaults to `false`. See [Synchronous and asynchronous delegation](#synchronous-and-asynchronous-delegation). |
| `timeout_ms`       | No       | Total budget for one call, including polling. Defaults to the assistant's tool timeout.                       |
| `poll_interval_ms` | No       | How often to poll a task that has not settled. Defaults to `500`.                                             |
| `messages`         | No       | Filler messages spoken while the call is in progress.                                                         |

### URL rules

The URL must use `http://` or `https://` and resolve to a host on the public internet. Internal destinations — `localhost`, private and reserved IP ranges, `.local` domains — are rejected when the assistant is saved, as are hostnames written in abbreviated, hexadecimal, or octal numeric form.

The hostname may not contain a `{{...}}` placeholder: the host a placeholder resolves to cannot be checked at save time. Placeholders in the path are fine, because the host stays fixed.

***

## Authenticate to the agent

Header values are stored exactly as written and resolved per conversation. A value can be:

* a literal, such as `Bearer abc123`
* a `{{dynamic_variable}}`, resolved from the conversation's [dynamic variables](/docs/inference/ai-assistants/dynamic-variables)
* an `{{#integration_secret}}identifier{{/integration_secret}}` section, resolved from a stored [integration secret](/docs/inference/ai-assistants/integrations)

Prefer an integration secret for anything long-lived: the secret itself never enters the assistant configuration, so it is never returned when you read the assistant back.

A2A headers do **not** accept the encrypted `{{variable | encryption_secret_ref}}` form used for [per-caller credentials](/docs/inference/ai-assistants/per-caller-credentials). That reference is resolved only in an MCP server's `api_key_ref` and in tool webhook header values; using it here is rejected with a `422` when you save the assistant. For a value that has to vary by caller, return it as a plain `{{dynamic_variable}}` from the dynamic variables webhook — bearing in mind that plain dynamic variables are not encrypted and are also interpolated into instructions and messages.

<Note>
  A header whose name or value resolves to an empty string is dropped for that conversation. If an agent starts returning `401`, check that every placeholder in its headers actually resolves.
</Note>

***

## Synchronous and asynchronous delegation

By default a delegation is synchronous: the turn waits for the remote agent, up to `timeout_ms`. If the agent answers with a task that is still running, it is polled every `poll_interval_ms` until it settles, and cancelled if the budget runs out.

Set `"async": true` when the agent is slow enough that waiting would leave dead air. The model is told the request is on its way and the turn completes immediately; when the answer lands it is added to the conversation without interrupting the caller, and the assistant picks it up on its next turn.

```json theme={null}
{
  "name": "research_agent",
  "url": "https://agents.example.com/research",
  "async": true
}
```

Use synchronous delegation when the answer is the caller's next sentence, and asynchronous delegation when the caller can keep talking without it. For a synchronous agent that takes a few seconds, `messages` fills the silence the same way [webhook filler messages](/docs/inference/ai-assistants/filler-messages) do:

```json theme={null}
{
  "name": "billing_agent",
  "url": "https://agents.example.com/billing",
  "messages": [
    { "type": "request_start", "content": "Let me check that with our billing team." },
    { "type": "request_response_delayed", "content": "Still looking, one moment.", "timing_ms": 4000 }
  ]
}
```

Filler messages are not used when `async` is `true` — there is no silence to fill.

***

## Tool names

Derived names are `a2a_<name>_<skill_id>`, truncated at 64 characters. Characters outside `[A-Za-z0-9_]` in the agent name are replaced with `_` before the name is built, so `billing agent` and `billing_agent` produce the same tool names and cannot both be configured on one assistant — saving the second is rejected.

The 43-character limit on `name` exists for the same reason: a longer name would consume the whole 64-character budget, leaving every skill on that agent with the same truncated tool name.

If a derived name still collides with another tool the assistant already has, a `_2`, `_3`, … suffix is added so that dispatch stays unambiguous.

***

## Limits

These are applied when the conversation starts, not when the assistant is saved. Anything past them is dropped silently, so keep configurations inside them:

| Limit                                             | Value     |
| ------------------------------------------------- | --------- |
| Agents per assistant                              | 64        |
| Skills read per card                              | 64        |
| Tools derived per assistant                       | 128       |
| Budget for all card fetches at conversation start | 6 seconds |
| Agent card size                                   | 1 MB      |
| Skill description, truncated past this            | 2 KB      |
| Skill ID, skill skipped past this                 | 512 bytes |
| Agent reply text kept                             | 8 KB      |

Agents are read in the order you configure them and the limits are applied in that order, so put the agents that matter most first.

***

## When an agent is unavailable

A card that cannot be fetched, times out, or advertises no skills yields no tools. **The assistant loses that capability for the conversation; it does not lose the call.** If the model is asked to do something only that agent could do, it will say it cannot rather than fail.

Once a tool exists and a call to it fails, the failure is reported to the model as ordinary text — "The agent did not answer in time", "The agent could not be reached" — so the assistant can acknowledge it and move on.

That failure mode is quiet by design, which is why a malformed configuration is rejected at save time instead: a typo you can see in a `422` is better than a tool that silently never appears.

### Troubleshooting

| Symptom                                | Check                                                                                                                                                                                                             |
| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The agent's tools never appear         | Fetch the resolved card URL yourself with the same headers — your `url` unchanged if its path ends in `.json`, otherwise `<url>/.well-known/agent-card.json`. It must return `200` with a card that lists skills. |
| Only some skills appear                | The card may advertise more than 64 skills, or the assistant may already be at its 128-tool ceiling.                                                                                                              |
| Tools appear but every call fails      | Check the agent's JSON-RPC endpoint — the one advertised on the card, which is not necessarily the card's own URL.                                                                                                |
| Changes to the card take a few minutes | Cards are cached for five minutes.                                                                                                                                                                                |

***

## API reference

* [Create an assistant](/api-reference/assistants/create-an-assistant)
* [Update an assistant](/api-reference/assistants/update-an-assistant)
