Skip to content

A2A & agent card

Chorus exposes a version-negotiated A2A JSON-RPC adapter over its native task, run, and delivery model. A2A tasks are authoritative Chorus execution tasks—not legacy task-shaped signals.

Capability Support
Protocol versions 0.3, 1.0
Empty or absent A2A-Version 0.3 compatibility default
Transport JSON-RPC over POST /a2a (and /a2a/)
Streaming / task subscription Not advertised; -32004
Push notifications Not advertised; -32003
Extended agent card Not advertised; -32004

The A2A-Version header alone selects the contract. Chorus never guesses a version from a method name or malformed payload. Send exactly 1.0 for v1 or 0.3 for the legacy adapter; any other non-empty value returns the standard VersionNotSupportedError (-32009). Responses include Vary: A2A-Version.

The reviewed contracts are pinned in test/fixtures/a2a-contract.json:

Contract Pin
JavaScript SDK @a2a-js/sdk@1.0.0-beta.0 plus lockfile integrity
A2A TCK 5996b79f9cefa6fc390980e383e358a66fb9e49e
A2A specification snapshot 173695755607e884aa9acf8ce4feed90e32727a1

GET /.well-known/agent-card.json is public. It advertises v1 first and v0.3 second as JSON-RPC interfaces, the Bearer authentication scheme, and one safe generic native-task skill. It does not expose the instance’s identities or internal roles.

The discovery endpoint follows the same A2A-Version rules. Its headerless response is the pure legacy v0.3 projection. Clients that explicitly request v1 receive the v1 representation and negotiate through supportedInterfaces. Version-specific ETags, Cache-Control, and 304 revalidation prevent one version’s card from being reused as another.

v1.0 method v0.3 method Native behavior
SendMessage message/send Create or continue an execution task
GetTask tasks/get Project task, latest run, history, and deliveries
ListTasks — List only visible, owned tasks with cursor pagination
CancelTask tasks/cancel Cancel through the canonical fenced task command

v1 uses PascalCase operations and ProtoJSON field/enum shapes. v0.3 uses slash methods and its legacy message/part/task shapes. The compatibility adapter is isolated from the canonical v1 service.

The caller must be a ring member. Supply the ring in message metadata or a data part; when omitted, Chorus uses the caller’s first visible ring.

Terminal window
curl -X POST "$CHORUS_URL/a2a" \
-H "Authorization: Bearer $CHORUS_API_KEY" \
-H "A2A-Version: 1.0" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "SendMessage",
"params": {
"message": {
"messageId": "release-audit-01",
"role": "ROLE_USER",
"parts": [{"text": "Audit the release boundary"}],
"metadata": {"ring": "ring:dev"}
},
"configuration": {"historyLength": 10, "returnImmediately": true}
}
}'

For a follow-up, resend with a new messageId plus the returned taskId and contextId. Replaying the same message ID and content is idempotent. Reusing a message ID with different content or a different Chorus ring, crossing contexts or rings, or reviving a terminal task is rejected as invalid parameters. Stable task, context, message, and transition IDs make retries converge on one execution graph.

Follow-up history and its idempotency fingerprints are appended in one storage transaction, so concurrent requests through different Chorus replicas cannot overwrite one another. A task retains at most 100 accepted A2A messages and at most 8 MiB of encoded history. At either limit, an exact replay still returns the existing task, while a new unique message or a same-ID/different-content collision returns invalid parameters. Entries are not evicted, so an old retained message ID never becomes silently acceptable again.

ListTasks pushes owner, ring, context, status, inclusive timestamp, and exclusive cursor predicates into storage. One transaction obtains the exact pre-pagination total and at most one bounded keyset page; delivery artifacts are loaded in one batch only when requested. Results use updated_at DESC, id DESC; opaque page tokens bind the caller and filters to the last returned (updated_at, id) pair. Tokens remain usable if that cursor task is subsequently updated or removed from the filter because pagination does not look the old row up again.

Response projection is bounded as well as input. A single encoded task is limited to 2 MiB (plus a small JSON-RPC envelope allowance). A ListTasks page is limited to 4 MiB of encoded tasks, 100 projected delivery artifacts, and 1,000 artifact parts. Each task projects at most 20 native deliveries and each delivery artifact at most 20 parts. When the next task would cross a page budget, Chorus returns the bounded prefix and anchors nextPageToken to the last task actually emitted; the unreturned task remains first on the next keyset page. Positive historyLength remains an upper bound as required by A2A, so Chorus may return fewer recent messages to keep one task finite. For ListTasks, an unset historyLength defaults to zero. Storage uses a lightweight task projection that omits unrelated flexible metadata; an explicit positive history request reads only one bounded recent entry per task in a smaller internal keyset batch. This prevents retained-history bytes from being multiplied by a 100-task inventory page before wire projection begins.

Native deliveries are a bounded Chorus-to-A2A projection rather than a separately pageable A2A collection. Clients that need every delivery or artifact reference should use the native Chorus execution APIs. Legacy task metadata is validated before SDK conversion: corrupt, deeply nested, or oversized retained messages are skipped, and invalid/oversized stored context IDs fall back to the deterministic native-task context.

Bearer authentication runs before parsing can mutate task state. Non-admin callers can only get, list, continue, and cancel tasks they created in rings they can currently see. Foreign-owner and cross-ring IDs are deliberately indistinguishable from missing tasks.

Idle cancellation is idempotent. Active work may be canceled only by the exact current lease holder with matching lease and dispatcher generations. This uses the same canonical command and fencing rules as Chorus HTTP, SDK, and MCP task paths.

Chorus streams and caps request bodies at 1,500,000 bytes, rejects ambiguous media/encoding headers and malformed UTF-8, and then validates the JSON-RPC envelope, version-specific parameters, parts, IDs, nested JSON bounds, strict RFC3339 timestamps, pagination, and terminal-state rules before the SDK converters run. A2A errors use standard numeric codes and include google.rpc.ErrorInfo in error.data. JSON-RPC and Agent Card bodies are not decorated with Chorus’s normal response meta object.

CI runs both official SDK clients and a real loopback Chorus server against the pinned upstream TCK. The selected upstream profile covers:

  • Agent Card structure, interface declarations, cache control, and ETags;
  • non-streaming JSON-RPC envelope, content type, method, and service headers;
  • standard A2A error codes and google.rpc.ErrorInfo.

The gate excludes cases whose preconditions Chorus does not advertise: gRPC, HTTP+JSON, streaming, push notifications, and extended cards. It also excludes the TCK’s special tck-* executor scenarios, which require a test agent that manufactures prescribed terminal/input-required/artifact sequences; Chorus’s adapter submits real native work and does not embed test-only execution semantics. Native lifecycle, authorization, idempotency, race, pagination, and generation-fenced cancellation are covered by Chorus’s own contract suites.

Run the local gates with a pinned TCK checkout:

Terminal window
bun run a2a:contract
uv_path="$(type -P uv)"
test -n "$uv_path"
A2A_TCK_DIR=/path/to/a2a-tck S22_UV_BIN="$uv_path" bun run a2a:tck

GET /health exposes the same supported-version and capability matrix for operator diagnostics.