NylorunDocsBeta
A2A

A2A client

Send tasks to an Agent over A2A from your server, answer its approvals, read its extensions, and catch up from the change feed.

npm install @nylorun/agents
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 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

OptionMeaning
urlThe agent's A2A endpoint: ${runtime}/v1/a2a/agents/${agentId} on the Runtime, or another agent's JSON-RPC URL.
keyAn application key, sent as Authorization: Bearer.
subjectWho 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.
scopesThe subject's scopes, sent as Nylorun-Scopes. Default ["sessions:own"].
headersMore headers for every request, such as a partner endpoint's own key.
fetchYour own fetch.
extensionsThe extension URIs to activate. Default: all five. A2A_EXTENSIONS names each URI.
cardUrlWhere 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

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,
});
FieldMeaning
messageIdThe message's idempotency key. A retry with the same id answers with the same task.
contextIdThe conversation: one session per subject, Agent, and contextId. Omit it to start a new one; read it back from task.contextId.
taskIdA task to continue, such as one waiting on input.
partstext, data, raw, and url parts.
metadataThe message's metadata.
extensionsExtension URIs the message's metadata uses.
deliveryWhat a message to a busy context does: queue (the default) or reject. Sent as metadata.delivery.
pushA push config: url, and optionally token, authentication, and id. See Push notifications.
returnImmediatelyAnswer at once with the task, instead of waiting.
historyLength, acceptedOutputModesAs in A2A's SendMessage configuration.
traceparentA W3C trace context the turn joins. See telemetry/v1.
signalAn 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

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();
CallA2A methodReturns
get(taskId, { historyLength? })GetTaskThe 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 pageAn async iterator over every task list matches.
cancel(taskId)CancelTaskThe task, TASK_STATE_CANCELED.
card()The Agent CardThe agent's card, with its extensions and capabilities.
call(method, params)AnyOne JSON-RPC call, such as GetTaskPushNotificationConfig.

Each is one JSON-RPC request to POST /v1/a2a/agents/{agentId}, and the card is GET /v1/a2a/agents/{agentId}/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

An approval, a question, or a connection request pauses the task under interactions/v1. Answer on the same task:

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

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.

ReaderReturns
readInteraction(source)interactions/v1: { interactionId, kind, prompt?, detail? }
readProgress(source)progress/v1: { steps }
readTelemetry(source)telemetry/v1: { totals?, traceId? }
readEffects(source)effects/v1: { uncertain? }
readFiles(source)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

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.

The change feed

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> 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().

OptionMeaning
baseUrlThe Runtime's URL.
keyAn application key. Alone, it reads the whole Tenant.
subject, scopesAct for a subject: only that subject's sessions' turns.
changedAfterThe previous page's cursor.
sessionIdOne session's turns instead, oldest first.
limit1 to 500. Default 100.

It needs the Host feature turns. Each turn:

FieldMeaning
turnId, sessionId, agentIdThe turn, its session, and its Agent.
statusrunning, paused, completed, failed, or cancelled.
startedAt, endedAtISO 8601 times. endedAt is null until the turn ends.
changedAtWhen the turn last changed.
totals{ steps, tokens, costUsd }, or null.
a2a{ contextId, taskId } for a turn that is an A2A task, else null.

Errors

A JSON-RPC error from the agent throws A2aError:

FieldMeaning
codeThe JSON-RPC or A2A code, such as -32001 for a task that is not found.
messageThe agent's message.
reasonThe google.rpc.ErrorInfo reason, such as TASK_NOT_FOUND or CONTEXT_BUSY.
metadataThe 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 in the Agents SDK reference.

On this page