Skip to content

A2A endpoints ​

Every endpoint the A2A handler exposes. Served on port 7870 by default.

Well-known paths ​

PathReturns
GET /.well-known/agent-card.jsonThe agent card as JSON
GET /.well-known/agent.jsonAlias for the card; some clients expect this path

Both paths return identical content. Serving both is a spec compatibility hedge — early A2A clients (including older @a2a-js/sdk versions) probed different paths.

Fleet multi-tenancy (/agents/<slug>/…) ​

A hub instance (one running a fleet of workspace members — ADR 0042) hosts each member as an independently-addressable A2A tenant under a URL prefix, implementing the A2A URL-based routing pattern. The hub reverse-proxies /agents/<slug>/* to the member's own server, so every path in this reference is reachable per-member:

PathReaches
GET /agents/<slug>/.well-known/agent-card.jsonthe member's card
POST /agents/<slug>/a2athe member's JSON-RPC endpoint (its task queue)

<slug> is the member's workspace id (e.g. matt-7689); host targets the hub itself. Routing to a member is pure transport — the hub agent's own reasoning loop is not invoked — so dispatching to a member's tenant path is a direct call, not a delegate_to through the hub agent.

Because a member self-advertises this tenant URL in its card (see supportedInterfaces), a peer that discovers the member's card routes to it correctly with no special client logic — the spec's "read the card, follow its URL" contract. The hub is the single auth boundary (ADR 0089): a caller presents any credential the hub accepts — its A2A_AUTH_TOKEN, an API key, or a per-device token — to /agents/<slug>/a2a. The hub authenticates it, and the proxy then swaps the Authorization header for the internal fleet service token before forwarding to the loopback-bound member. It does not forward the caller's credential verbatim, because each member has its own instance_root and therefore its own device registry and auth.token — a hub-minted device token would be structurally 401'd by every sister. The swap is operator-tier only and preserves the member-public anonymization, so unauthenticated callers still reach only a member's declared public paths.

JSON-RPC methods (POST /a2a) ​

All methods use JSON-RPC 2.0 envelopes:

json
{
  "jsonrpc": "2.0",
  "id": "<caller-chosen>",
  "method": "<name>",
  "params": { ... }
}

message/send — blocking ​

Submit a message and wait for the terminal task. Returns the full Task object including artifacts.

json
{
  "method": "message/send",
  "params": {
    "message": {
      "role": "user",
      "parts": [{"text": "summarize https://example.com"}]
    },
    "metadata": {"skill": "summarize_pr"}
  }
}

Result shape:

json
{
  "result": {
    "kind": "task",
    "id": "<task-id>",
    "contextId": "<ctx-id>",
    "status": {"state": "completed"},
    "artifacts": [...],
    "data": {
      "usage": {"input_tokens": 1200, "output_tokens": 340, "total_tokens": 1540},
      "durationMs": 4230
    }
  }
}

The kind: "task" discriminator is required — @a2a-js/sdk routes by it.

message/stream — SSE ​

Same as message/send but streams frames as the run progresses. One SSE event per frame. Every frame carries a kind discriminator:

kindWhen emitted
taskFirst frame — initial task state
status-updateState transitions, tool-start / tool-end progress, and the input-required pause (with final: true)
artifact-updateStreaming partial outputs

Consumers must check kind before interpreting fields — without it, @a2a-js/sdk's for await loop silently skips frames.

tasks/get ​

json
{"method": "tasks/get", "params": {"id": "<task-id>"}}

Returns the current state of a task. Use to poll when push notifications aren't wired.

Task records are persisted (instance-scoped a2a-tasks.db, 24h TTL, write-through on create + every terminal transition), so tasks/get and tasks/resubscribe answer with the final state + artifacts even after the in-memory copy is evicted (1h) or the process restarts. The background runner doesn't survive a restart, so any task still non-terminal at boot is marked failed ("interrupted by server restart") rather than left hanging.

tasks/resubscribe ​

json
{"method": "tasks/resubscribe", "params": {"id": "<task-id>"}}

SSE stream of remaining frames for an in-flight task. Lets a consumer reconnect after a network blip without losing events.

Human-in-the-loop (input-required) ​

The agent can pause mid-task to ask the operator a question — the spec's input-required flow (ADR 0003). It's driven by the lead-agent ask_human tool, which issues a LangGraph interrupt(); the graph checkpoints at that exact point.

  1. The task transitions to input-required — a status-update whose status.message carries the question, with final: true, closing the SSE cycle. The task is not terminal; it's parked (and persisted, so it survives a restart). Webhook consumers get an immediate (un-throttled) push.
  2. The caller answers by sending a message/send (or message/stream) carrying the same taskId (and contextId) with the reply as a text part. protoAgent resumes the graph via Command(resume=…) from the checkpoint — continuing exactly where ask_human paused — and drives to a terminal state (or another input-required).
json
{"method": "message/stream", "params": {
  "message": {"taskId": "<parked-task-id>", "contextId": "<ctx>",
              "parts": [{"kind": "text", "text": "approved"}]}}}

A message with no taskId (or one that isn't input-required) starts a fresh task as usual. HITL is not advertised as a card extension — the form rides a application/vnd.protolabs.hitl-v1+json DataPart on the INPUT_REQUIRED frame, and a consumer discovers it by reading the part's mimeType. (Earlier revisions of this page and ADR 0014 referred to a hitl-mode-v1 card capability; no such extension is declared or implemented.)

tasks/cancel ​

json
{"method": "tasks/cancel", "params": {"id": "<task-id>"}}

Transitions the task to canceled if it's still running. No-op on terminal tasks.

agent/getAuthenticatedExtendedCard ​

json
{"method": "agent/getAuthenticatedExtendedCard", "params": {}}

Returns the agent card to an authenticated caller (the request has already passed the bearer/api-key check). Same shape as the public /.well-known/agent-card.json.

tasks/pushNotificationConfig/{set,get,list,delete} ​

Register webhooks so a non-streaming consumer is kept updated as work progresses. The agent POSTs to the webhook on every meaningful transition the caller cares about — the initial working, each per-tool progress step, and the terminal state — mirroring what SSE subscribers see. Non-terminal updates are throttled to at most one POST per ~1.5s (carrying the latest state) so a burst of tool events can't storm the webhook; terminal transitions flush immediately. The COMPLETED webhook also carries the terminal artifact.

json
{
  "method": "tasks/pushNotificationConfig/set",
  "params": {
    "taskId": "<task-id>",
    "pushNotificationConfig": {
      "url": "https://consumer/callback/abc",
      "token": "shared-secret"
    }
  }
}

The handler accepts both token shapes the A2A spec permits:

ShapeJSON
Top-level token (what @a2a-js/sdk serializes by default){"url": "...", "token": "..."}
Structured authentication.credentials (RFC-8821){"url": "...", "authentication": {"schemes": ["Bearer"], "credentials": "..."}}

Both produce Authorization: Bearer <token> and the spec-canonical X-A2A-Notification-Token: <token> header on outgoing webhook POSTs, so a strict receiver can validate the notification belongs to a config it created. When both token shapes are present, top-level wins.

Webhook payload: a TaskStatusUpdateEvent (the same envelope as the matching SSE status-update frame); the terminal/COMPLETED POST attaches the full artifact. Delivery retries 3× with exponential backoff (1s/3s/9s), skipping retry on 4xx.

Registered configs are persisted (write-through to an instance-scoped a2a-push.db, 24h TTL) so they survive the task's terminal eviction and a process restart.

REST aliases ​

Thin REST wrappers are also exposed for non-JSON-RPC clients:

Method + PathEquivalent to
POST /a2a/tasks/:taskId/pushNotificationConfigtasks/pushNotificationConfig/set
GET /a2a/tasks/:taskId/pushNotificationConfigtasks/pushNotificationConfig/list
GET /a2a/tasks/:taskIdtasks/get

Same semantics, same token-shape parsing, same SSRF guarding.

SSRF guard ​

Outgoing webhook URLs are resolved once and checked against an allowlist before the handler accepts a push config. By default, private IP ranges (RFC1918 + loopback + link-local) are refused.

To permit trusted docker-network hostnames, set:

bash
PUSH_NOTIFICATION_ALLOWED_HOSTS=automaker-server,internal-hooks
PUSH_NOTIFICATION_ALLOWED_CIDRS=10.0.0.0/8,172.16.0.0/12

Hosts in PUSH_NOTIFICATION_ALLOWED_HOSTS bypass the DNS check entirely.

Extension advertisement ​

The card's capabilities.extensions array declares protocol extensions the agent implements. See Extensions reference for the ones the template ships.

Part of the protoLabs autonomous development studio.