# A2A client (/docs/a2a/client)



```sh
npm install @nylorun/agents
```

```ts
import { createA2aClient } from "@nylorun/agents/a2a";

const a2a = createA2aClient({
  url: `${process.env.NYLORUN_RUNTIME_URL}/v1/a2a/agents/support`,
  key: process.env.NYLORUN_SERVER_KEY!,
  subject: "a2a:billing",
});

const task = await a2a.send({
  messageId: "refund:demo-123",
  contextId: "ticket-4821",
  parts: [{ text: "Refund order demo-123." }],
  returnImmediately: true,
  push: { url: "https://app.example.com/a2a/push", token: pushToken },
});
```

`createA2aClient()` speaks A2A 1.0 JSON-RPC to one agent: an Agent on the
Runtime, an endpoint `createA2aHandler()` serves, or any other A2A 1.0 agent.
It sends `A2A-Version: 1.0` and activates all five
[profile v2](/docs/a2a/profile/v2) extensions with `A2A-Extensions`. It is in
`@nylorun/agents` 0.31.0-beta or later, from `@nylorun/agents/a2a` and from
`@nylorun/agents`. Run it on your server: it carries an application key.

## Create a client [#create-a-client]

| Option       | Meaning                                                                                                                                                     |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url`        | The agent's A2A endpoint: `${runtime}/v1/a2a/agents/${agentId}` on the Runtime, or another agent's JSON-RPC URL.                                            |
| `key`        | An application key, sent as `Authorization: Bearer`.                                                                                                        |
| `subject`    | Who the tasks belong to, sent as `Nylorun-Subject`. The Runtime needs one with an application key: a caller sees only its own subject's contexts and tasks. |
| `scopes`     | The subject's scopes, sent as `Nylorun-Scopes`. Default `["sessions:own"]`.                                                                                 |
| `headers`    | More headers for every request, such as a partner endpoint's own key.                                                                                       |
| `fetch`      | Your own `fetch`.                                                                                                                                           |
| `extensions` | The extension URIs to activate. Default: all five. `A2A_EXTENSIONS` names each URI.                                                                         |
| `cardUrl`    | Where the Agent Card is. Default: `${url}/card` for a Runtime endpoint, else `${url}/.well-known/agent-card.json`.                                          |

Keep the subject stable for one caller: it owns the contexts, and another
subject's task answers as not found.

## Send a task [#send-a-task]

```ts
const task = await a2a.send({
  messageId: "refund:demo-123",
  contextId: "ticket-4821",
  parts: [{ text: "Refund order demo-123." }],
  returnImmediately: true,
  push: { url: "https://app.example.com/a2a/push", token: pushToken },
  traceparent,
});
```

| Field                                  | Meaning                                                                                                                            |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `messageId`                            | The message's idempotency key. A retry with the same id answers with the same task.                                                |
| `contextId`                            | The conversation: one session per subject, Agent, and `contextId`. Omit it to start a new one; read it back from `task.contextId`. |
| `taskId`                               | A task to continue, such as one waiting on input.                                                                                  |
| `parts`                                | `text`, `data`, `raw`, and `url` parts.                                                                                            |
| `metadata`                             | The message's metadata.                                                                                                            |
| `extensions`                           | Extension URIs the message's metadata uses.                                                                                        |
| `delivery`                             | What a message to a busy context does: `queue` (the default) or `reject`. Sent as `metadata.delivery`.                             |
| `push`                                 | A push config: `url`, and optionally `token`, `authentication`, and `id`. See [Push notifications](/docs/a2a/push).                |
| `returnImmediately`                    | Answer at once with the task, instead of waiting.                                                                                  |
| `historyLength`, `acceptedOutputModes` | As in A2A's `SendMessage` configuration.                                                                                           |
| `traceparent`                          | A W3C trace context the turn joins. See [`telemetry/v1`](/docs/a2a/telemetry/v1).                                                  |
| `signal`                               | An `AbortSignal`.                                                                                                                  |

`send()` resolves to the A2A `Task`. Without `returnImmediately`, it waits
until the task ends or pauses, up to the Runtime limit `a2aWaitMs` (5 minutes
by default), then answers with the task still `TASK_STATE_WORKING`. For long
work, pass `returnImmediately` and a push config.

A message to a context whose task is still running is queued: `send()`
answers with the task that will take it, `TASK_STATE_SUBMITTED`, under the
queued message's id until a turn takes it. With `delivery: "reject"`, the
client throws `A2aError` with `reason` `CONTEXT_BUSY` instead.

## Read, list, and cancel [#read-list-and-cancel]

```ts
const current = await a2a.get(task.id, { historyLength: 10 });

const page = await a2a.list({
  contextId: "ticket-4821",
  status: "TASK_STATE_WORKING",
  statusTimestampAfter: "2026-11-02T09:00:00Z",
  pageSize: 50,
});

for await (const each of a2a.listAll({ contextId: "ticket-4821" })) console.log(each.id, each.status.state);

await a2a.cancel(task.id);

const card = await a2a.card();
```

| Call                                                                                          | A2A method                   | Returns                                                                                                                                                                                                          |
| --------------------------------------------------------------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `get(taskId, { historyLength? })`                                                             | `GetTask`                    | The task, with its latest plan, telemetry once it ended, and its artifacts. `history` only with `historyLength`.                                                                                                 |
| `list({ contextId?, status?, statusTimestampAfter?, pageSize?, pageToken?, historyLength? })` | `ListTasks`                  | `{ tasks, nextPageToken, pageSize, totalSize }`: the subject's tasks on this Agent, latest status change first. `pageSize` is at most 100, 50 by default. Pass `nextPageToken` as `pageToken` for the next page. |
| `listAll(options)`                                                                            | `ListTasks`, page after page | An async iterator over every task `list` matches.                                                                                                                                                                |
| `cancel(taskId)`                                                                              | `CancelTask`                 | The task, `TASK_STATE_CANCELED`.                                                                                                                                                                                 |
| `card()`                                                                                      | The Agent Card               | The agent's card, with its extensions and `capabilities`.                                                                                                                                                        |
| `call(method, params)`                                                                        | Any                          | One JSON-RPC call, such as `GetTaskPushNotificationConfig`.                                                                                                                                                      |

Each is one JSON-RPC request to
[`POST /v1/a2a/agents/{agentId}`](/reference/runtime/sessions/a2a/call-an-agent-over-a2a),
and the card is [`GET /v1/a2a/agents/{agentId}/card`](/reference/runtime/sessions/a2a/get-an-agents-a2a-card).
A queued message that no turn has taken yet is not a task `ListTasks` lists,
and it cannot be cancelled until it starts: `CancelTask` answers
`TaskNotCancelableError`. A task that already ended answers the same.

## Answer an interaction [#answer-an-interaction]

An approval, a question, or a connection request pauses the task under
[`interactions/v1`](/docs/a2a/interactions/v1). Answer on the same task:

```ts
import { readInteraction } from "@nylorun/agents/a2a";

const open = readInteraction(task);
if (open?.kind === "approval") {
  await a2a.answer({
    taskId: task.id,
    contextId: task.contextId,
    interactionId: open.interactionId,
    approved: true,
    messageId: `answer:${open.interactionId}`,
  });
}
```

`answer()` takes exactly one of `approved`, for an `approval`, and `value`,
for a `question` or a `connection` (`{ connected: true }`). The `messageId`
makes the answer safe to retry.

## Read the extensions [#read-the-extensions]

Each reader takes whatever you hold: a push's `StreamResponse`, a task, a
status update, a status, a message, or an artifact. It looks in that object's
own `metadata`, then its status message's, and returns `undefined` when the
data is missing or not the profile's shape. A core A2A agent's tasks read as
plain A2A.

| Reader                       | Returns                                                                                                                                                                             |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `readInteraction(source)`    | [`interactions/v1`](/docs/a2a/interactions/v1): `{ interactionId, kind, prompt?, detail? }`                                                                                         |
| `readProgress(source)`       | [`progress/v1`](/docs/a2a/progress/v1): `{ steps }`                                                                                                                                 |
| `readTelemetry(source)`      | [`telemetry/v1`](/docs/a2a/telemetry/v1): `{ totals?, traceId? }`                                                                                                                   |
| `readEffects(source)`        | [`effects/v1`](/docs/a2a/effects/v1): `{ uncertain? }`                                                                                                                              |
| `readFiles(source)`          | [`files/v1`](/docs/a2a/files/v1): an array of `{ artifact, file }`, one for each artifact with file data in a task, an artifact update, or one artifact. Empty when there are none. |
| `readExtension(source, uri)` | The raw data under any extension URI.                                                                                                                                               |

## Verify pushes [#verify-pushes]

```ts
import { readFiles, readProgress, readTelemetry, verifyA2aPush } from "@nylorun/agents/a2a";

export async function POST(request: Request) {
  const update = await verifyA2aPush({
    secret: process.env.A2A_PUSH_SECRET!,
    headers: request.headers,
    body: await request.text(),
    token: pushToken,
  });

  const steps = readProgress(update)?.steps;
  const totals = readTelemetry(update)?.totals;
  for (const { file } of readFiles(update)) await saveFile(file.path, file.version, file.sha256);
  await saveStatus(update, { steps, totals });
  return new Response(null, { status: 204 });
}
```

`verifyA2aPush()` resolves to the parsed delivery, or throws
`A2aPushVerificationError` with a `reason`. Signing, retries, and ordering are
on [Push notifications](/docs/a2a/push#verify-a-delivery).

## The change feed [#the-change-feed]

```ts
import { listTurnChanges } from "@nylorun/agents/a2a";

let cursor = await loadCursor(); // undefined the first time
let hasMore = true;
while (hasMore) {
  const page = await listTurnChanges({
    baseUrl: process.env.NYLORUN_RUNTIME_URL!,
    key: process.env.NYLORUN_SERVER_KEY!,
    changedAfter: cursor,
    limit: 100,
  });
  for (const turn of page.turns) if (turn.a2a) await reconcile(turn);
  cursor = page.cursor;
  hasMore = page.hasMore;
  await saveCursor(cursor);
}
```

Pushes can be lost: a receiver down for more than a day, or one that refused
them. The change feed is the backstop. `listTurnChanges()` calls
[`GET /v1/turns?changedAfter=<cursor>`](/reference/runtime/sessions/follow-turns-as-they-change)
and returns the turns that changed after the cursor, in the order they
changed. A turn appears again each time it changes. Store `cursor` and pass it
as `changedAfter` next time; it comes back even when `turns` is empty. Without
`changedAfter`, the feed starts from the beginning. One call covers every task,
so a receiver can run it on a timer and when it starts, then read any task it
is unsure of with `a2a.get()`.

| Option              | Meaning                                                 |
| ------------------- | ------------------------------------------------------- |
| `baseUrl`           | The Runtime's URL.                                      |
| `key`               | An application key. Alone, it reads the whole Tenant.   |
| `subject`, `scopes` | Act for a subject: only that subject's sessions' turns. |
| `changedAfter`      | The previous page's `cursor`.                           |
| `sessionId`         | One session's turns instead, oldest first.              |
| `limit`             | 1 to 500. Default 100.                                  |

It needs the Host feature `turns`. Each turn:

| Field                            | Meaning                                                              |
| -------------------------------- | -------------------------------------------------------------------- |
| `turnId`, `sessionId`, `agentId` | The turn, its session, and its Agent.                                |
| `status`                         | `running`, `paused`, `completed`, `failed`, or `cancelled`.          |
| `startedAt`, `endedAt`           | ISO 8601 times. `endedAt` is `null` until the turn ends.             |
| `changedAt`                      | When the turn last changed.                                          |
| `totals`                         | `{ steps, tokens, costUsd }`, or `null`.                             |
| `a2a`                            | `{ contextId, taskId }` for a turn that is an A2A task, else `null`. |

## Errors [#errors]

A JSON-RPC error from the agent throws `A2aError`:

| Field      | Meaning                                                                        |
| ---------- | ------------------------------------------------------------------------------ |
| `code`     | The JSON-RPC or A2A code, such as `-32001` for a task that is not found.       |
| `message`  | The agent's message.                                                           |
| `reason`   | The `google.rpc.ErrorInfo` reason, such as `TASK_NOT_FOUND` or `CONTEXT_BUSY`. |
| `metadata` | The `ErrorInfo` metadata, such as the running task's id.                       |

An HTTP refusal, such as `401`, `404` for an unknown Agent, or `429`, throws
the SDK's `RuntimeError`; see [Errors](/reference/runtime/agents-sdk#errors)
in the Agents SDK reference.
