Skip to main content
This document defines the canonical structure for AdCP responses transmitted over the A2A protocol.

A2A Wire Format

Examples below use A2A 1.0 wire format: Parts carry no kind discriminator (content type is implied by which field is set — text, data, url, or raw), roles are ROLE_USER / ROLE_AGENT, and task states are TASK_STATE_* (ProtoJSON canonical). See the A2A Guide for a side-by-side with v0.3. There are two distinct status layers. A2A task.status.state describes transport execution; the AdCP DataPart’s top-level status describes the task-specific protocol result. They usually align, but they are not aliases. For example, a structured get_products business rejection uses A2A TASK_STATE_COMPLETED and DataPart status: "rejected": the invocation completed and produced a deliberate commercial-refusal result. The AdCP A2A Profile Extension v3 also pins an AdCP status: "submitted" response inside an A2A TASK_STATE_COMPLETED Task. Native A2A TASK_STATE_SUBMITTED is an interim transport state before the AdCP handler returns; it is not the mapping for queued AdCP work. For v0.3 servers, the same DataPart becomes { "kind": "data", "data": {...} } and states become lowercase. Extraction clients accept both shapes during the compatibility period.

Required Structure

Final Responses (status: “completed”)

AdCP responses over A2A MUST:
  • Include at least one DataPart (a Part carrying a non-null data field) containing the task response payload
  • Use single artifact with multiple parts (not multiple artifacts)
  • Use the last DataPart as authoritative when multiple data parts exist
  • NOT wrap AdCP payloads in custom framework objects (no { response: {...} } wrappers)
Recommended non-streaming SendMessageResponse pattern:
  • TextPart (Part with text field): Human-readable summary — recommended but optional
  • DataPart (Part with data field): Structured AdCP response payload — required
  • FilePart (Part with url or raw field): Optional file references (previews, reports)
AdCP profile responses use exactly one artifact. Put related TextParts, DataParts, and FileParts in that artifact; model fundamentally separate deliverables as separate AdCP tasks.

AdCP Submitted Responses (A2A completed)

When the AdCP handler returns status: "submitted", the handler invocation is finished even though the durable AdCP operation is queued. The profile therefore uses an A2A completed Task and carries the Submitted response in the artifact:
The A2A Task id and AdCP task_id are independent. The AdCP handle MUST remain only in the direct DataPart; do not copy it to artifact.metadata.adcp_task_id. Poll by sending a new profile invocation with skill: "get_task_status" and input.task_id: "adcp-task-9a21", not by polling the completed A2A Task.

Interim A2A Responses (working, native submitted, input-required, auth-required)

Interim status updates are delivered as TaskStatusUpdateEvent, with optional progress/challenge data carried in status.message.parts[] (not in artifacts). Artifacts accumulate during the task lifecycle but are read as the final deliverable once the task reaches a terminal state.
When delivered over SSE or as a push notification, this event is wrapped in the A2A 1.0 StreamResponse oneof: { "statusUpdate": { … } }. Non-streaming responses such as native A2A Get Task deliver the bare object. Clients unwrap before reading status.state — see A2A Response Extraction. Interim response characteristics:
  • TextPart is recommended for human-readable status
  • DataPart is optional but follows AdCP schemas when provided
  • Interim status schemas (*-async-response-working.json, *-async-response-input-required.json, etc.) are work-in-progress and may evolve
  • Implementors may choose to handle interim data more loosely given schema evolution
When a final A2A transport state is reached (completed, failed, canceled, or native A2A rejected), the full AdCP task response is delivered on a Task object with the DataPart in .artifacts[0].parts[].

Framework Wrappers (NOT PERMITTED)

CRITICAL: DataPart content MUST be the direct AdCP response payload, not wrapped in framework-specific objects.
Why this matters:
  • Breaks schema validation (clients expect products at root, not response.products)
  • Adds unnecessary nesting layer
  • Violates protocol-agnostic design (wrapper is framework-specific)
  • Complicates client extraction code
If your implementation adds wrappers, this is a bug that should be fixed in the framework layer, not worked around in client code.

Canonical Client Behavior

This section defines EXACTLY how clients MUST extract AdCP responses from A2A protocol responses.

Quick Reference

Key Insights:
  • Final statuses use Task object with data in .artifacts. If a server has no structured payload (e.g., JSON-RPC parse error, pre-task auth failure), it may place only a text message in status.message.parts — clients fall back to that location.
  • Interim statuses use TaskStatusUpdateEvent with optional data in status.message.parts[].
  • Stream/webhook delivery wraps the payload in the A2A 1.0 StreamResponse oneof ({ task }, { statusUpdate }, { artifactUpdate }, { message }). Clients unwrap before reading fields.
  • All statuses use AdCP schemas when data is present.
  • Interim status schemas are work-in-progress and may evolve.

Rule 1: Status-Based Handling

Clients MUST branch on the normalized A2A transport state to determine the correct data extraction location. The raw wire value at status.state is TASK_STATE_COMPLETED in 1.0 or completed in v0.3. Normalize before comparing, extract the DataPart, and then branch separately on the AdCP payload’s own status; never replace that payload status with the transport state. See A2A Response Extraction.
Critical:
  • Interim statuses use TaskStatusUpdateEvent → extract from status.message.parts[]
  • Final statuses use Task object → extract from .artifacts[0].parts[], falling back to status.message.parts[] if artifacts are empty

Rule 2: Data Extraction Helpers

Extract data from the appropriate location based on webhook type:
These detectors work for both wire formats: a 1.0 DataPart has data set (no kind), a v0.3 DataPart has kind: "data" and data set — both satisfy p.data != null.

Rule 3: Schema Validation

All AdCP responses use schemas, but validation approach varies by status:
Schema Evolution Note: Interim status schemas (*-async-response-working.json, etc.) are work-in-progress. Implementors may choose to handle these more loosely while schemas stabilize.

Complete Example

Putting it all together with proper handling of both Task and TaskStatusUpdateEvent payloads:

Last Data Part Authority Pattern

Test Cases

✅ Correct Behavior

❌ Incorrect Behavior (Common Mistakes)

Error Handling

A schema-valid business outcome, including a partial result with an errors[] member, is still a completed A2A invocation. A non-streaming SendMessage response selects the task branch:
A fatal failure after execution starts uses a failed Task and carries structured error data in its artifact. Pre-task routing failures use the A2A binding error mechanism.

Webhook Payloads

A2A push notifications use a StreamResponse branch. Terminal delivery wraps the same Task structure used by SendMessageResponse:
Interim delivery selects statusUpdate, and its server-authored Message carries its own messageId plus the matching taskId and contextId:

File Parts in Responses

File references may accompany the authoritative DataPart inside the Task artifact. They never replace the typed AdCP response:

Retry and Idempotency

A continuation of an input-required A2A Task sends a new Message containing a new messageId, the existing taskId and contextId, and a complete typed profile invocation. Ordinary AdCP idempotency rules still apply to the task input. Native GetTask observes an A2A transport Task; it does not replace get_task_status for a durable AdCP operation.

Implementation Checklist

When implementing A2A responses for AdCP: Final Responses (status: “completed” or “failed”) - Use Task object:
  • Always include status field from TaskState enum
  • Use .artifacts array with at least one DataPart containing AdCP response payload
  • Include TextPart with human-readable message (recommended for UX)
  • Use single artifact with multiple parts (not multiple artifacts)
  • Use last DataPart as authoritative if multiple exist
  • Never nest AdCP data in custom wrappers (no { response: {...} } objects)
  • DataPart content MUST match AdCP schemas (validate against [task]-response.json)
  • Map an AdCP status: "submitted" response to A2A completed, keep task_id only in the DataPart, and direct clients to the AdCP get_task_status task
Interim A2A Responses (status: “working”, native “submitted”, “input-required”) - Use TaskStatusUpdateEvent:
  • Use status.message.parts[] for optional data (not .artifacts)
  • TextPart is recommended for human-readable status updates
  • DataPart is optional but follows AdCP schemas when provided ([task]-async-response-[status].json)
  • Interim schemas are work-in-progress - clients may handle more loosely
  • Include progress indicators when applicable (percentage, current_step, ETA)
Error Handling:
  • Use status: "failed" for protocol errors only (auth, invalid params, system errors)
  • Use errors array for task failures (platform auth, partial data) with status: "completed"
General:
  • Include taskId and contextId for tracking
  • Follow discriminated union patterns for task responses (check schemas)
  • Use correct payload type: Task for final states, TaskStatusUpdateEvent for interim
  • Support taskId-based deduplication for retry detection

See Also