> ## Documentation Index
> Fetch the complete documentation index at: https://docs.adcontextprotocol.org/llms.txt
> Use this file to discover all available pages before exploring further.

# A2A Response Format

> A2A response format for AdCP: required DataPart structure, artifact layout for completed and async tasks, and status-specific response patterns over Agent-to-Agent Protocol.

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](/dist/docs/3.2.0-beta.0/building/by-layer/L0/a2a-guide#a2a-protocol-versions) 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](/dist/docs/3.2.0-beta.0/building/by-layer/L0/a2a-profile-extension) 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:**

```json theme={null}
{
  "task": {
    "id": "task_123",
    "contextId": "ctx_456",
    "status": {
      "state": "TASK_STATE_COMPLETED"
    },
    "artifacts": [{
      "artifactId": "task-result",
      "name": "task_result",
      "parts": [
        {
          "text": "Found 12 video products perfect for pet food campaigns"
        },
        {
          "data": {
            "status": "completed",
            "cache_scope": "account",
            "products": [...],
            "total": 12
          }
        }
      ]
    }]
  }
}
```

* **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:

```json theme={null}
{
  "task": {
    "id": "a2a-task-create-42",
    "contextId": "ctx-create-42",
    "status": { "state": "TASK_STATE_COMPLETED" },
    "artifacts": [{
      "artifactId": "adcp-result",
      "parts": [{
        "data": {
          "status": "submitted",
          "task_id": "adcp-task-9a21",
          "message": "Awaiting IO signature"
        }
      }]
    }]
  }
}
```

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.

```json theme={null}
{
  "taskId": "task_123",
  "contextId": "ctx_456",
  "status": {
    "state": "TASK_STATE_WORKING",
    "timestamp": "2026-01-22T10:15:00.000Z",
    "message": {
      "messageId": "msg-progress-001",
      "taskId": "task_123",
      "contextId": "ctx_456",
      "role": "ROLE_AGENT",
      "parts": [
        {
          "text": "Processing your request. Analyzing 50,000 inventory records..."
        },
        {
          "data": {
            "percentage": 45,
            "current_step": "analyzing_inventory"
          }
        }
      ]
    }
  }
}
```

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](/dist/docs/3.2.0-beta.0/building/by-layer/L0/a2a-response-extraction#extraction-algorithm).

**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.

```json theme={null}
// ❌ WRONG - Wrapped in custom object
{
  "data": {
    "response": {           // ← Framework wrapper
      "products": [...]
    }
  }
}

// ✅ CORRECT - Direct AdCP payload
{
  "data": {
    "status": "completed",
    "cache_scope": "account",
    "products": []
  }
}
```

**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

| Status                 | Webhook Type            | Data Location                                       | Schema Required?          | Returns                              |
| ---------------------- | ----------------------- | --------------------------------------------------- | ------------------------- | ------------------------------------ |
| `working`              | `TaskStatusUpdateEvent` | `status.message.parts[]`                            | ✅ Yes (if present)        | `{ status, taskId, message, data? }` |
| native A2A `submitted` | `TaskStatusUpdateEvent` | `status.message.parts[]`                            | ✅ Yes (if present)        | `{ status, taskId, message, data? }` |
| `input-required`       | `TaskStatusUpdateEvent` | `status.message.parts[]`                            | ✅ Yes (if present)        | `{ status, taskId, message, data? }` |
| `auth-required` (1.0)  | `TaskStatusUpdateEvent` | `status.message.parts[]`                            | ✅ Yes (auth challenge)    | `{ status, taskId, message, data }`  |
| `completed`            | `Task`                  | `.artifacts[]` (fallback: `status.message.parts[]`) | ✅ Required                | `{ status, taskId, message, data }`  |
| `failed`               | `Task`                  | `.artifacts[]` (fallback: `status.message.parts[]`) | ✅ Required                | `{ status, taskId, message, data }`  |
| `rejected` (1.0)       | `Task`                  | `.artifacts[]`                                      | ✅ Required (`adcp_error`) | `{ status, taskId, message, data }`  |

**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](/dist/docs/3.2.0-beta.0/building/by-layer/L0/a2a-response-extraction#extraction-algorithm).

```javascript theme={null}
const INTERIM = ['working', 'submitted', 'input-required', 'auth-required'];
const FINAL = ['completed', 'failed', 'canceled', 'rejected'];

function handleA2aResponse(response) {
  const status = response.status; // normalized A2A transport state

  // INTERIM STATUSES - Extract from status.message.parts (TaskStatusUpdateEvent)
  if (INTERIM.includes(status)) {
    return {
      status: status,
      taskId: response.taskId,
      contextId: response.contextId,
      message: extractTextPartFromMessage(response),
      data: extractDataPartFromMessage(response),  // Optional AdCP data (required for auth-required)
    };
  }

  // FINAL STATUSES - Extract from .artifacts (Task object), fallback to status.message
  if (FINAL.includes(status)) {
    return {
      status: status,
      taskId: response.taskId,
      contextId: response.contextId,
      message: extractTextPartFromArtifacts(response) ?? extractTextPartFromMessage(response),
      data: extractDataPartFromArtifacts(response) ?? extractDataPartFromMessage(response),
    };
  }

  // Forward-compatible: unknown future states return null, do not throw
  return { status, taskId: response.taskId, contextId: response.contextId, message: null, data: null };
}
```

**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:

```javascript theme={null}
// Part-type detectors: field presence (A2A 1.0) with kind fallback (v0.3)
const isDataPart = (p) =>
  p.data != null && typeof p.data === 'object' && !Array.isArray(p.data);
const isTextPart = (p) => typeof p.text === 'string';

// For FINAL statuses (Task object) - extract from .artifacts, return null if absent
function extractDataPartFromArtifacts(response) {
  const dataParts = response.artifacts?.[0]?.parts?.filter(isDataPart) || [];
  if (dataParts.length === 0) return null;  // caller falls back to status.message.parts

  // Use LAST data part as authoritative
  const lastDataPart = dataParts[dataParts.length - 1];
  const payload = lastDataPart.data;

  // CRITICAL: Payload MUST be direct AdCP response, not a framework wrapper.
  // A wrapper is a single-key object { response: {...} } — reject it.
  // Objects that have 'response' alongside other keys are NOT wrappers.
  const keys = Object.keys(payload);
  if (keys.length === 1 && keys[0] === 'response' && typeof payload.response === 'object') {
    throw new Error(
      'Invalid response format: DataPart contains wrapper object. ' +
      'Expected direct AdCP payload (e.g., {products: [...]}) ' +
      'but received {response: {products: [...]}}. ' +
      'This is a server-side bug that must be fixed.'
    );
  }

  return payload;
}

function extractTextPartFromArtifacts(response) {
  const textPart = response.artifacts?.[0]?.parts?.find(isTextPart);
  return textPart?.text || null;
}

// For INTERIM statuses (TaskStatusUpdateEvent) - extract from status.message.parts
function extractDataPartFromMessage(response) {
  const dataPart = response.status?.message?.parts?.find(isDataPart);
  return dataPart?.data || null;
}

function extractTextPartFromMessage(response) {
  const textPart = response.status?.message?.parts?.find(isTextPart);
  return textPart?.text || null;
}
```

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:

```javascript theme={null}
function validateResponse(response, taskName) {
  const status = response.status;
  let data, schemaName;

  // Extract data and determine schema based on status
  if (INTERIM.includes(status)) {
    // INTERIM: Optional data from status.message.parts
    data = extractDataPartFromMessage(response);

    if (data) {
      // Interim status has its own schema (work-in-progress)
      schemaName = `${taskName}-async-response-${status}.json`;

      // Optional: Implementors may skip interim validation as schemas evolve
      if (STRICT_VALIDATION_MODE) {
        validateAgainstSchema(data, loadSchema(schemaName));
      }
    }
  } else if (FINAL.includes(status)) {
    // FINAL: Required data from .artifacts (fallback to status.message.parts)
    data = extractDataPartFromArtifacts(response) ?? extractDataPartFromMessage(response);
    schemaName = `${taskName}-response.json`;

    // Required: Final responses must validate
    if (!validateAgainstSchema(data, loadSchema(schemaName))) {
      throw new Error(
        `Response payload does not match ${taskName} schema. ` +
        `Ensure DataPart contains direct AdCP response structure.`
      );
    }
  }
}
```

**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:

```javascript theme={null}
async function executeTask(taskName, params) {
  const response = await a2aClient.send({
    message: {
      messageId: crypto.randomUUID(),
      role: 'ROLE_USER',
      parts: [{ data: { skill: taskName, input: params } }]
    }
  });

  // 1. Status-based handling (extracts from correct location)
  const result = handleA2aResponse(response);

  // 2. Schema validation
  validateResponse(response, taskName);

  return result;
}

// Usage
const result = await executeTask('get_products', {
  brief: 'CTV inventory in California'
});

// Handle different response types
if (result.status === 'working') {
  // TaskStatusUpdateEvent - data from status.message.parts
  console.log('Processing:', result.message);
  if (result.data) {
    console.log('Progress:', result.data.percentage + '%');
  }
} else if (result.status === 'input-required') {
  // TaskStatusUpdateEvent - data from status.message.parts
  console.log('Input needed:', result.message);
  console.log('Reason:', result.data?.reason);
} else if (result.status === 'completed') {
  // Task object - data from .artifacts
  console.log('Success:', result.message);
  console.log('Products:', result.data.products); // Full AdCP response
}
```

## Last Data Part Authority Pattern

<details>
  <summary><strong>Why this pattern?</strong></summary>

  An artifact can accumulate multiple DataParts while streaming. Only the final
  Task artifact uses last-DataPart authority:

  ```json theme={null}
  {
    "task": {
      "id": "task_123",
      "contextId": "ctx_456",
      "status": { "state": "TASK_STATE_COMPLETED" },
      "artifacts": [{
        "artifactId": "product-result",
        "parts": [
          {"text": "Found 12 products"},
          {"data": {"progress": 25}},
          {"data": {"status": "completed", "cache_scope": "account", "products": [...], "total": 12}}
        ]
      }]
    }
  }
  ```

  **Note:** This is an AdCP-specific convention, not required by A2A protocol. Document this in your Agent Card when serving non-AdCP clients.
</details>

## Test Cases

### ✅ Correct Behavior

```javascript theme={null}
// Test 1: Working status (TaskStatusUpdateEvent) - extract from status.message.parts
const workingResponse = {
  taskId: 'task_123',
  contextId: 'ctx_456',
  status: {
    state: 'TASK_STATE_WORKING',
    message: {
      messageId: 'msg-progress-001',
      taskId: 'task_123',
      contextId: 'ctx_456',
      role: 'ROLE_AGENT',
      parts: [
        { text: 'Processing inventory...' },
        { data: { percentage: 50, current_step: 'analyzing' } }
      ]
    }
  }
};

const result1 = handleA2aResponse(workingResponse);
assert(result1.data.percentage === 50, 'Should extract data from status.message.parts');
assert(result1.message === 'Processing inventory...', 'Should extract text from status.message.parts');

// Test 2: Completed status (Task) - extract from .artifacts
const completedResponse = {
  id: 'task_123',
  contextId: 'ctx_456',
  status: {
    state: 'TASK_STATE_COMPLETED',
    timestamp: '2026-01-22T10:30:00.000Z'
  },
  artifacts: [{
    artifactId: 'product-result',
    parts: [
      { text: 'Found 3 products' },
      { data: { status: 'completed', cache_scope: 'account', products: [...], total: 3 } }
    ]
  }]
};

const result2 = handleA2aResponse(completedResponse);
assert(result2.data !== undefined, 'Completed status must have data');
assert(Array.isArray(result2.data.products), 'Data should be direct AdCP payload');

// Test 3: Wrapper detection (should reject)
const wrappedResponse = {
  id: 'task_123',
  status: { state: 'TASK_STATE_COMPLETED' },
  artifacts: [{
    artifactId: 'product-result',
    parts: [
      { data: { response: { products: [...] } } }
    ]
  }]
};

assert.throws(() => {
  extractDataPartFromArtifacts(wrappedResponse);
}, /Invalid response format.*wrapper/);
```

### ❌ Incorrect Behavior (Common Mistakes)

```javascript theme={null}
// WRONG: Extracting from wrong location for interim status
function badHandleWorking(response) {
  // ❌ TaskStatusUpdateEvent doesn't have .artifacts - data is in status.message.parts
  const data = response.artifacts?.[0]?.parts?.find(isDataPart)?.data;
  return { status: 'working', data }; // Will be null/undefined!
}

// WRONG: Extracting from wrong location for completed status
function badHandleCompleted(response) {
  // ❌ Task object has data in .artifacts, not in status.message.parts
  const data = response.status?.message?.parts?.find(p => p.data)?.data;
  return { status: 'completed', data }; // Will be null/undefined!
}

// WRONG: Not checking for wrappers
function badExtraction(response) {
  const payload = response.artifacts[0].parts[0].data;
  // ❌ Returns { response: { products: [...] } } instead of { products: [...] }
  return payload; // Client receives wrong structure!
}

// WRONG: Accessing nested response field
function badClientUsage(result) {
  // ❌ Client code shouldn't need to do this
  const products = result.data.response.products;
  // Should be: result.data.products
}
```

## 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:

```json theme={null}
{
  "task": {
    "id": "task_123",
    "contextId": "ctx_456",
    "status": { "state": "TASK_STATE_COMPLETED" },
    "artifacts": [{
      "artifactId": "signal-result",
      "parts": [{
        "data": {
          "status": "completed",
          "signals": [],
          "errors": [{
            "code": "NO_DATA_IN_REGION",
            "message": "No signal data available for Australia"
          }]
        }
      }]
    }]
  }
}
```

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.

```json theme={null}
{
  "task": {
    "id": "task_456",
    "contextId": "ctx_456",
    "status": { "state": "TASK_STATE_FAILED" },
    "artifacts": [{
      "artifactId": "adcp-error",
      "parts": [{
        "data": {
          "adcp_error": {
            "code": "AUTHENTICATION_FAILED",
            "message": "The API token is invalid or expired"
          }
        }
      }]
    }]
  }
}
```

| Situation                                                                          | A2A state              | Data location                         |
| ---------------------------------------------------------------------------------- | ---------------------- | ------------------------------------- |
| Schema-valid success, partial result, business rejection, or AdCP Submitted result | `TASK_STATE_COMPLETED` | `task.artifacts[0].parts[]`           |
| Fatal failure after execution starts                                               | `TASK_STATE_FAILED`    | `task.artifacts[0].parts[]`           |
| Native A2A policy rejection                                                        | `TASK_STATE_REJECTED`  | `task.artifacts[0].parts[]`           |
| Working, input-required, auth-required, or native submitted update                 | matching interim state | `statusUpdate.status.message.parts[]` |

## Webhook Payloads

A2A push notifications use a `StreamResponse` branch. Terminal delivery wraps
the same Task structure used by `SendMessageResponse`:

```json theme={null}
{
  "task": {
    "id": "task_123",
    "contextId": "ctx_456",
    "status": {
      "state": "TASK_STATE_COMPLETED",
      "timestamp": "2026-01-22T10:30:00.000Z"
    },
    "artifacts": [{
      "artifactId": "media-buy-result",
      "parts": [
        { "text": "Media buy approved and live" },
        { "data": { "status": "completed", "media_buy_id": "mb_456", "confirmed_at": "2026-01-22T10:30:00.000Z", "revision": 1, "packages": [] } }
      ]
    }]
  }
}
```

Interim delivery selects `statusUpdate`, and its server-authored Message carries
its own `messageId` plus the matching `taskId` and `contextId`:

```json theme={null}
{
  "statusUpdate": {
    "taskId": "task_123",
    "contextId": "ctx_456",
    "status": {
      "state": "TASK_STATE_INPUT_REQUIRED",
      "timestamp": "2026-01-22T10:20:00.000Z",
      "message": {
        "messageId": "msg-input-001",
        "taskId": "task_123",
        "contextId": "ctx_456",
        "role": "ROLE_AGENT",
        "parts": [
          { "text": "Select a supported campaign start time." },
          { "data": { "reason": "START_TIME_REQUIRED" } }
        ]
      }
    }
  }
}
```

## File Parts in Responses

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

```json theme={null}
{
  "task": {
    "id": "task_creative_789",
    "contextId": "ctx_creative_789",
    "status": { "state": "TASK_STATE_COMPLETED" },
    "artifacts": [{
      "artifactId": "creative-result",
      "parts": [
        { "data": { "status": "completed", "creative_id": "cr_789" } },
        { "url": "https://cdn.example.com/cr_789/preview.mp4", "filename": "preview.mp4", "mediaType": "video/mp4" }
      ]
    }]
  }
}
```

## 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

* [A2A Guide](/dist/docs/3.2.0-beta.0/building/by-layer/L0/a2a-guide) - Complete A2A integration guide
* [Task Lifecycle](/dist/docs/3.2.0-beta.0/building/by-layer/L3/task-lifecycle) - Status handling patterns
* [Error Handling](/dist/docs/3.2.0-beta.0/building/by-layer/L3/error-handling) - Fatal vs non-fatal errors
* [Protocol Comparison](/dist/docs/3.2.0-beta.0/building/concepts/protocol-comparison) - MCP vs A2A differences
