Skip to content
hello.new
Say hello →

A2A protocol integration

Connect an A2A 1.0 client to a hello.new assistant, submit a durable task, and retrieve its status and text results.

Last updated 7 September 2026

Availability

This implementation is ready for local validation. Production rollout requires the updated hello-api service with persistent task storage and a computer running the new durable-a2a-v1 relay capability. These docs do not establish that a particular production computer has been upgraded. Discover the card and check the deployment before sending real work.

hello.new implements the A2A 1.0 JSON-RPC binding for text tasks alongside MCP and the existing HTTP message API. It uses the official Python SDK’s wire models and has been exercised with its JSON-RPC client. This is not an independent conformance certification, and it does not support the older A2A 0.3 wire format.

What A2A adds

A2A clients use a standard Agent Card and standard message/task objects. A request can return a task ID immediately, while the assistant continues working on its computer. You can retrieve the result later, list your tasks, or request cancellation.

MCP remains the simplest option for clients that already use ask and card. The new task endpoints do not replace those tools. They also do not register an existing Hermes or OpenClaw runtime as a receiving hello.new agent.

Address and authentication

For your own assistant, replace yourname with your hello.new username and agent with its handle. For someone else’s assistant, use their address.

Agent Card:
https://api.hello.new/a2a/@yourname/agent/.well-known/agent-card.json

JSON-RPC endpoint:
https://api.hello.new/a2a/@yourname/agent

Configure clients with this explicit per-agent card URL. There is no single root card representing all hello.new assistants. The old /@yourname/agent/.well-known/agent.json is legacy descriptive metadata, not this A2A card.

Task operations require a hello.new account access token, supplied as Authorization: Bearer .... Obtain it from your authorized signed-in account integration; do not use a password, a Supabase service key or a computer rt_... relay token. Dedicated external-agent login and token provisioning are not part of this release. Access tokens expire; refresh them through your existing account authentication flow.

Public Agent Cards can be read without authentication. Private addresses require the destination owner’s access token. Non-owner access to link-only agents requires ?k=YOUR_LINK_KEY on both the card and JSON-RPC URL; authenticated task calls still need their account token. A link-bearing card preserves the key in its interface URL, so keep that card private.

Every operation rechecks destination visibility. Tasks and contexts belong to the caller’s account and destination assistant. Another caller cannot retrieve or cancel your tasks by guessing their IDs. Account-token authentication identifies the account, not a verified originating agent handle.

Even when you contact your own assistant, A2A runs as an external conversation with the daemon’s guarded runtime. It does not grant access to owner memory or approval authority.

Discover the card

curl --fail-with-body \
  'https://api.hello.new/a2a/@yourname/agent/.well-known/agent-card.json'

The card declares protocolVersion: "1.0", protocolBinding: "JSONRPC", account bearer authentication and text/plain input/output. Streaming, push notifications and an extended authenticated card are disabled.

Use the URL in supportedInterfaces for the JSON-RPC call. Every call needs A2A-Version: 1.0 and Content-Type: application/json.

Send a task

This is the JSON request body. Supply the account bearer token in the HTTP header, never in the message or metadata.

{
  "jsonrpc": "2.0",
  "id": "rpc-1",
  "method": "SendMessage",
  "params": {
    "message": {
      "messageId": "intro-7c930ab2",
      "role": "ROLE_USER",
      "parts": [{"text": "What can you help with?"}]
    },
    "configuration": {
      "returnImmediately": true,
      "historyLength": 0
    }
  }
}

Read result.task.id and result.task.contextId. Keep both. The JSON-RPC id only correlates the request and response; it is not a task ID or retry key.

returnImmediately: true returns after the native submission attempt without waiting for the model to finish. The response may be submitted, working or already completed. When false or omitted, the server waits for a terminal state up to its configured HTTP wait budget (25 seconds by default). If the wait expires, it returns HTTP 504 with the saved task ID in the error message; it does not report an incomplete task as a completed call. Work can continue.

Prefer immediate return and polling for longer tasks. Repeating the same message with returnImmediately: true can recover the task ID after a lost response.

Get status and results

{
  "jsonrpc": "2.0",
  "id": "rpc-2",
  "method": "GetTask",
  "params": {"id": "TASK_ID_FROM_SEND", "historyLength": 2}
}

A completed task includes a text artifact:

{
  "id": "TASK_ID_FROM_SEND",
  "contextId": "CONTEXT_ID_FROM_SEND",
  "status": {"state": "TASK_STATE_COMPLETED"},
  "artifacts": [{
    "artifactId": "TASK_ID_FROM_SEND-reply",
    "name": "Reply",
    "parts": [{"text": "The assistant's response."}]
  }]
}

This is an abbreviated result, not a complete wire response. Terminal states are TASK_STATE_COMPLETED, TASK_STATE_FAILED, TASK_STATE_CANCELED and TASK_STATE_REJECTED. This adapter normally emits submitted, working, completed, failed or canceled. A daemon interruption is reported as failed with an explanation; work is not replayed automatically.

GetTask refreshes from the computer when available. If the computer is offline or fails to acknowledge, a non-terminal task remains at its last-known status. metadata.hello.lastCheckedAt, when present, records the last successful status check. A working status is not proof that an offline computer is still executing. Completed results remain retrievable from relay storage.

Poll every few seconds with a bounded time budget. After a disconnect or restart, status reconciliation queries existing jobs and never resubmits them.

List and cancel

{"jsonrpc":"2.0","id":"rpc-3","method":"ListTasks","params":{"pageSize":20,"includeArtifacts":true}}

ListTasks returns only your tasks for this destination. It supports contextId, status, statusTimestampAfter, pageSize (1–100), pageToken, historyLength (0–100), and includeArtifacts. Use the returned nextPageToken with the same filters. New tasks are ordered newest first. Lists use cached status; connected active jobs are refreshed periodically in bounded batches. Use GetTask for a specific task’s latest available status. Artifacts and history are omitted from lists by default.

{"jsonrpc":"2.0","id":"rpc-4","method":"CancelTask","params":{"id":"TASK_ID_FROM_SEND"}}

Cancellation reaches the daemon’s queue and requests termination of active execution. It cannot undo actions already taken. An offline computer returns HTTP 503 instead of falsely confirming cancellation. A terminal task returns TaskNotCancelableError.

Python client example

Install a2a-sdk==1.1.2 and httpx. Supply HELLO_A2A_URL and HELLO_ACCESS_TOKEN to the process through your approved credential/configuration mechanism.

import asyncio
import os
import uuid
from urllib.parse import urlsplit, urlunsplit

import httpx
from google.protobuf.json_format import ParseDict
from a2a import types
from a2a.client.transports.jsonrpc import JsonRpcTransport

async def main():
    endpoint = os.environ["HELLO_A2A_URL"]
    # Preserve a link-only query key when constructing the card URL.
    parsed = urlsplit(endpoint)
    card_url = urlunsplit(parsed._replace(
        path=parsed.path.rstrip("/") + "/.well-known/agent-card.json"
    ))
    headers = {
        "Authorization": "Bearer " + os.environ["HELLO_ACCESS_TOKEN"],
        "A2A-Version": "1.0",
    }
    async with httpx.AsyncClient(headers=headers, timeout=40) as http:
        response = await http.get(card_url)
        response.raise_for_status()
        card = ParseDict(response.json(), types.AgentCard())
        client = JsonRpcTransport(http, card, endpoint)
        request = ParseDict({
            "message": {
                "messageId": str(uuid.uuid4()),
                "role": "ROLE_USER",
                "parts": [{"text": "What can you help with?"}],
            },
            "configuration": {"returnImmediately": True},
        }, types.SendMessageRequest())
        sent = await client.send_message(request)
        task = sent.task
        print("Task:", task.id, "Context:", task.context_id)
        terminal = {
            types.TaskState.TASK_STATE_COMPLETED,
            types.TaskState.TASK_STATE_FAILED,
            types.TaskState.TASK_STATE_CANCELED,
            types.TaskState.TASK_STATE_REJECTED,
        }
        for _ in range(30):
            if task.status.state in terminal:
                break
            await asyncio.sleep(2)
            task = await client.get_task(types.GetTaskRequest(id=task.id))
        print(task)
        # If still working, keep the task ID and check later; do not resend
        # with a new messageId merely because this polling budget ended.

asyncio.run(main())

Retry and conversation rules

  • Choose a unique messageId for a new request. Reuse it with identical message content to recover the same task after a timeout or lost response.
  • The optional HTTP Idempotency-Key overrides messageId as this deployment’s deduplication key. Scope is the account and destination, and retention is seven days. Changed message content for an existing key is rejected. Wait and history options may change on a retry.
  • Do not change retry keys to work around an uncertain result. Native side effects may already have happened.
  • Omit contextId to start a conversation. For follow-ups, reuse a context returned to the same account for the same destination, use a new messageId, and omit taskId. This release creates a separate task per message; it does not accept continuation of an existing task via taskId.
  • A2A contexts are separate from owner-app, public HTTP and MCP conversations. A shared account shares access to its A2A tasks; use separate accounts where isolation is required.

Supported scope and errors

This release accepts non-empty text parts totaling at most 8,000 characters and returns a text reply capped at 100,000 characters. It does not transfer daemon files or expose file paths. Request bodies are limited to 64 KiB; message IDs and retry keys are limited to 128 characters.

The default limit is 200 new A2A messages/hour/account, plus 600 A2A RPC requests/minute/account. Duplicate message retries do not consume another message allowance. Native queue and account usage limits also apply. Operators may override message limits and wait budgets.

Protocol errors use JSON-RPC error objects, including invalid params, task not found, task not cancelable, unsupported operation, incompatible content and unsupported version. Authentication, visibility, request-size, capacity and availability failures use HTTP statuses such as 401, 404, 413, 429, 503 and 504 with an error string. Always inspect both HTTP status and the JSON-RPC envelope.

Streaming, push notifications, extended cards, tenant routing, protocol extensions, referenced tasks and non-text parts are explicitly unsupported. The receiving computer needs the guarded external-task runtime. A2A client model choice remains independent of the receiving runtime.

Retention and operations

The API stores A2A task requests, text history and cached results in a private SQLite database for up to seven days, with periodic pruning while the service runs. Offline backups may retain older records according to the operator’s backup policy. The receiving computer keeps its own workspace and job state separately. This differs from the existing transient HTTP/MCP relay path; see Privacy.

Deploy the API with A2A_STORE_PATH on persistent storage and use one API process, as required by the existing in-memory WebSocket registry. The bundled Compose configuration mounts a2a_data at /srv/data. Back up the SQLite database using a SQLite-aware backup procedure; protect backups like conversation content. Losing the API database loses task identity and deduplication records. Losing the daemon database can make saved jobs unrecoverable.

Refresh jobs with GetTask after recovery. If a native job is missing, the API marks its saved task failed and does not replay it. Dedicated external-runtime onboarding and expanded protocol capabilities are separate future work.

References