Skip to main content
このドキュメントは、A2A プロトコルで送信される AdCP レスポンスの 標準構造 を定義します。

必須構造

最終レスポンス(status: “completed”)

A2A 上の AdCP レスポンスは必ず以下を満たす必要があります:
  • タスクのペイロードを含む DataPart(kind: ‘data’)を少なくとも 1 つ含める
  • 複数アーティファクトではなく、1 つのアーティファクトに複数パートを入れる
  • DataPart が複数ある場合は最後のものを正とする
  • AdCP ペイロードをフレームワーク固有オブジェクトでラップしない({ response: {...} } など禁止)
Recommended pattern:
{
  "status": "completed",
  "taskId": "task_123",
  "contextId": "ctx_456",
  "artifacts": [{
    "name": "task_result",
    "parts": [
      {
        "kind": "text",
        "text": "Found 12 video products perfect for pet food campaigns"
      },
      {
        "kind": "data",
        "data": {
          "products": [...],
          "total": 12
        }
      }
    ]
  }]
}
  • TextPart (kind: ‘text’): 人間向けサマリー - 推奨(任意)
  • DataPart (kind: ‘data’): 構造化された AdCP レスポンスペイロード - 必須
  • FilePart (kind: ‘file’): 任意のファイル参照(プレビュー、レポート)
複数アーティファクト: 本質的に異なる成果物(例: クリエイティブと別個のトラフィッキングレポート)がある場合のみ。AdCP では稀であり、基本は 1 アーティファクト内に複数パートを推奨。

中間レスポンス(working, submitted, input-required)

中間ステータスのレスポンスは、進捗把握のために任意で AdCP 構造化データを含められます。
{
  "status": "working",
  "taskId": "task_123",
  "contextId": "ctx_456",
  "artifacts": [{
    "parts": [
      {
        "kind": "text",
        "text": "Processing your request. Analyzing 50,000 inventory records..."
      },
      {
        "kind": "data",
        "data": {
          "percentage": 45,
          "current_step": "analyzing_inventory"
        }
      }
    ]
  }]
}
中間レスポンスの特徴:
  • TextPart はステータス表示のため推奨
  • DataPart は任意だが、提供する場合は AdCP スキーマに準拠
  • 中間ステータス用スキーマ(*-async-response-working.json*-async-response-input-required.json など)は策定中で変わる可能性あり
  • スキーマ進化を踏まえ、中間データの扱いを緩やかにする選択も可能
最終ステータスになった場合status: "completed" または status: "failed")、.artifacts 内に AdCP タスクレスポンスを含む DataPart が必須になります。

フレームワークのラッパー(禁止)

重要: DataPart の内容はフレームワーク固有オブジェクトでラップせず、AdCP レスポンスペイロードを直接含める必要があります。
// ❌ WRONG - Wrapped in custom object
{
  "kind": "data",
  "data": {
    "response": {           // ← Framework wrapper
      "products": [...]
    }
  }
}

// ✅ CORRECT - Direct AdCP payload
{
  "kind": "data",
  "data": {
    "products": [...]       // ← Direct schema-compliant response
  }
}
理由:
  • スキーマ検証が破綻する(クライアントは products がルートにあると期待)
  • 不要なネストが増える
  • プロトコル非依存設計に反する(ラッパーがフレームワーク依存)
  • クライアントでのデータ抽出が複雑化
実装がラッパーを追加している場合、クライアント側で回避するのではなく、フレームワーク層のバグとして修正すべきです。

クライアントの標準的な扱い

このセクションでは、クライアントが A2A プロトコルレスポンスから AdCP レスポンスを抽出する方法を正確に定義します。

クイックリファレンス

StatusWebhook TypeData LocationSchema Required?Returns
workingTaskStatusUpdateEventstatus.message.parts[]✅ Yes (if present){ status, taskId, message, data? }
submittedTaskStatusUpdateEventstatus.message.parts[]✅ Yes (if present){ status, taskId, message, data? }
input-requiredTaskStatusUpdateEventstatus.message.parts[]✅ Yes (if present){ status, taskId, message, data? }
completedTask.artifacts[] only✅ Required{ status, taskId, message, data }
failedTask.artifacts[] only✅ Required{ status, taskId, message, data }
ポイント:
  • 最終ステータスTask オブジェクトを用い、データは .artifacts に格納
  • 中間ステータスTaskStatusUpdateEvent を用い、status.message.parts[] に任意データ
  • いずれのステータスもデータがある場合は AdCP スキーマを使用
  • 中間ステータスのスキーマは策定中で変わる可能性あり

ルール1: ステータスに応じた処理

Clients MUST branch on status field to determine the correct data extraction location:
function handleA2aResponse(response) {
  const status = response.status;

  // 中間ステータス - status.message.parts から抽出(TaskStatusUpdateEvent)
  if (['working', 'submitted', 'input-required'].includes(status)) {
    return {
      status: status,
      taskId: response.taskId,
      contextId: response.contextId,
      message: extractTextPartFromMessage(response),
      data: extractDataPartFromMessage(response),  // Optional AdCP data
    };
  }

  // 最終ステータス - .artifacts から抽出(Task オブジェクト)
  if (['completed', 'failed'].includes(status)) {
    return {
      status: status,
      taskId: response.taskId,
      contextId: response.contextId,
      message: extractTextPartFromArtifacts(response),
      data: extractDataPartFromArtifacts(response),  // Required AdCP payload
    };
  }

  throw new Error(`Unknown A2A status: ${status}`);
}
重要:
  • 中間ステータス: TaskStatusUpdateEventstatus.message.parts[] から抽出
  • 最終ステータス: Task オブジェクト → .artifacts[0].parts[] から抽出

Rule 2: Data Extraction Helpers

Extract data from the appropriate location based on webhook type:
// For FINAL statuses (Task object) - extract from .artifacts
function extractDataPartFromArtifacts(response) {
  const dataParts = response.artifacts?.[0]?.parts
    ?.filter(p => p.kind === 'data') || [];

  if (dataParts.length === 0) {
    throw new Error('Final response (completed/failed) missing required DataPart in artifacts');
  }

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

  // CRITICAL: Payload MUST be direct AdCP response
  if (payload.response !== undefined && 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(p => p.kind === 'text');
  return textPart?.text || null;
}

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

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

ルール3: スキーマ検証

すべての AdCP レスポンスはスキーマを用いますが、検証方法はステータスによって異なります:
function validateResponse(response, taskName) {
  const status = response.status;
  let data, schemaName;

  // ステータスに応じてデータを抽出しスキーマを決定
  if (['working', 'submitted', 'input-required'].includes(status)) {
    // 中間: status.message.parts の任意データ
    data = extractDataPartFromMessage(response);

    if (data) {
      // 中間ステータス専用スキーマ(策定中)
      schemaName = `${taskName}-async-response-${status}.json`;

      // 任意: スキーマが変わる可能性があるため中間検証を省略してもよい
      if (STRICT_VALIDATION_MODE) {
        validateAgainstSchema(data, loadSchema(schemaName));
      }
    }
  } else if (['completed', 'failed'].includes(status)) {
    // 最終: .artifacts から必須データ
    data = extractDataPartFromArtifacts(response);
    schemaName = `${taskName}-response.json`;

    // 最終レスポンスは必ず検証
    if (!validateAgainstSchema(data, loadSchema(schemaName))) {
      throw new Error(
        `Response payload does not match ${taskName} schema. ` +
        `Ensure DataPart contains direct AdCP response structure.`
      );
    }
  }
}
スキーマ進化の注意: 中間ステータスのスキーマ(*-async-response-working.json など)は策定中です。安定するまでは緩やかな扱いにする選択も可能です。

完全な例

Task と TaskStatusUpdateEvent の両方を正しく扱う統合例:
async function executeTask(taskName, params) {
  const response = await a2aClient.send({
    task: taskName,
    params: params
  });

// 1. ステータスに基づいて正しい場所から抽出
  const result = handleA2aResponse(response);

// 2. スキーマ検証
  validateResponse(response, taskName);

  return result;
}

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

// ステータス別の処理
if (result.status === 'working') {
  // TaskStatusUpdateEvent - data は 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 オブジェクト - data は .artifacts
  console.log('Success:', result.message);
  console.log('Products:', result.data.products); // Full AdCP response
}

Last Data Part Authority パターン

Test Cases

✅ Correct Behavior

// Test 1: Working status (TaskStatusUpdateEvent) - extract from status.message.parts
const workingResponse = {
  status: 'working',
  taskId: 'task_123',
  contextId: 'ctx_456',
  status: {
    state: 'working',
    message: {
      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 = {
  status: 'completed',
  taskId: 'task_123',
  contextId: 'ctx_456',
  status: {
    state: 'completed',
    timestamp: '2025-01-22T10:30:00Z'
  },
  artifacts: [{
    parts: [
      { kind: 'text', text: 'Found 3 products' },
      { kind: 'data', data: { 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 = {
  status: 'completed',
  taskId: 'task_123',
  artifacts: [{
    parts: [
      { kind: 'data', data: { response: { products: [...] } } }
    ]
  }]
};

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

❌ Incorrect Behavior (Common Mistakes)

// 誤り: 中間ステータスで抽出元を間違える
function badHandleWorking(response) {
  // ❌ TaskStatusUpdateEvent doesn't have .artifacts - data is in status.message.parts
  const data = response.artifacts?.[0]?.parts?.find(p => p.kind === 'data')?.data;
  return { status: 'working', data }; // Will be null/undefined!
}

// 誤り: completed で抽出元を間違える
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!
}

// 誤り: ラッパーを確認しない
function badExtraction(response) {
  const payload = response.artifacts[0].parts[0].data;
  // ❌ Returns { response: { products: [...] } } instead of { products: [...] }
  return payload; // Client receives wrong structure!
}

// 誤り: ネストされた response を参照
function badClientUsage(result) {
  // ❌ クライアントコードがこうする必要はない
  const products = result.data.response.products;
  // 正しくは: result.data.products
}

エラーハンドリング

タスクレベルのエラー(部分失敗)

タスクは実行されたが完全には完了しなかった場合。status: "completed" の DataPart に errors 配列を入れます:
{
  "status": "completed",
  "taskId": "task_123",
  "artifacts": [{
    "parts": [
      {
        "kind": "text",
        "text": "Signal discovery completed with partial results"
      },
      {
        "kind": "data",
        "data": {
          "signals": [...],
          "errors": [{
            "code": "NO_DATA_IN_REGION",
            "message": "No signal data available for Australia",
            "field": "deliver_to.countries[1]",
            "details": {
              "requested_country": "AU",
              "available_countries": ["US", "CA", "GB"]
            }
          }]
        }
      }
    ]
  }]
}
errors 配列を使う場面:
  • プラットフォーム認可の問題(PLATFORM_UNAUTHORIZED
  • データが部分的にしかない場合
  • データの一部でバリデーション問題がある場合

プロトコルレベルのエラー(致命的)

タスクが実行できなかった場合。status: "failed" とメッセージを返します:
{
  "taskId": "task_456",
  "status": "failed",
  "message": {
    "parts": [{
      "kind": "text",
      "text": "Authentication failed: Invalid or expired API token"
    }]
  }
}
status: failed を使う場面:
  • 認証失敗(無効/期限切れトークン)
  • リクエスト不正(JSON 破損、必須フィールド欠落)
  • リソース不在(未知の taskId、期限切れ context)
  • システムエラー(DB 不調、内部サービス障害)

ステータスマッピング

AdCP は A2A の TaskState enum をそのまま使用します:
A2A StatusPayload TypeData LocationAdCP Usage
completedTask.artifactsTask finished successfully, data in DataPart, optional errors array
failedTask.artifactsFatal error preventing completion, optional error details
input-requiredTaskStatusUpdateEventstatus.message.partsNeed user input/approval, data + text explaining what’s needed
workingTaskStatusUpdateEventstatus.message.partsProcessing (< 120s), optional progress data
submittedTaskStatusUpdateEventstatus.message.partsLong-running (hours/days), minimal data, use webhooks/polling

Webhook ペイロード

非同期処理(status: "submitted")では Webhook でも同じアーティファクト構造を返します:
POST /webhook-endpoint
{
  "taskId": "task_123",
  "status": "completed",
  "timestamp": "2025-01-22T10:30:00Z",
  "artifacts": [{
    "parts": [
      {"kind": "text", "text": "Media buy approved and live"},
      {"kind": "data", "data": {
        "media_buy_id": "mb_456",
        "packages": [...],
        "creative_deadline": "2025-01-30T23:59:59Z"
      }}
    ]
  }]
}
AdCP データは同じ Last DataPart パターンで抽出します。Webhook 認証、リトライパターン、セキュリティWebhooks を参照してください。

レスポンス内の File Part

クリエイティブ系の操作ではファイル参照を含む場合があります:
{
  "status": "completed",
  "artifacts": [{
    "parts": [
      {"kind": "text", "text": "Creative uploaded and preview generated"},
      {"kind": "data", "data": {
        "creative_id": "cr_789",
        "format_id": {
          "agent_url": "https://creatives.adcontextprotocol.org",
          "id": "video_standard_30s"
        },
        "status": "ready"
      }},
      {"kind": "file", "uri": "https://cdn.example.com/cr_789/preview.mp4", "name": "preview.mp4", "mimeType": "video/mp4"}
    ]
  }]
}
File Part の用途: プレビュー URL、生成済みアセット、トラフィッキングレポート。AdCP レスポンスの生データには使わず、必ず DataPart を使用。

リトライと冪等性

TaskId による重複排除

A2A の taskId はリトライ検出に使えます。エージェントは次を行うべきです:
  • taskId が完了済みオペレーションと一致する場合(TTL 内)、キャッシュレスポンスを返す
  • 進行中のオペレーションに対する重複 taskId 送信は拒否する
// Duplicate taskId during active operation
{
  "taskId": "task_123",
  "status": "failed",
  "message": {
    "parts": [{
      "kind": "text",
      "text": "Task 'task_123' is already in progress. Use tasks/get to check status."
    }]
  }
}

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)
Interim Responses (status: “working”, “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