Skip to main content
AdCP のオペレーションは秒から日までかかることがあります。本ガイドでは各タイプの扱い方と非同期前提の設計を説明します。

オペレーションの種類

AdCP のオペレーションは次の 3 種に分類されます:

1. 同期オペレーション

completed または failed で即時応答:
OperationDescription
get_adcp_capabilitiesAgent capability discovery
list_creative_formatsFormat catalog
外部システムを必要としない高速な処理です。

2. 対話的オペレーション

処理継続前に input-required を返す場合があります:
OperationDescription
get_productsWhen brief is vague or needs clarification
create_media_buyWhen approval is required
進行にはユーザー入力が必要です。

3. 非同期オペレーション

working または submitted を返し、ポーリング/ストリーミングが必要:
OperationDescription
create_media_buyCreates campaigns with external systems
sync_creativesUploads and processes creative assets
get_productsComplex inventory searches
activate_signalActivates audience segments
外部システム連携や人の承認を伴う処理です。

タイムアウト設定

オペレーション種別に応じて妥当なタイムアウトを設定します:
const TIMEOUTS = {
  sync: 30_000,         // 30 seconds for immediate operations
  interactive: 300_000,  // 5 minutes for human input
  working: 120_000,      // 2 minutes for working tasks
  submitted: 86_400_000  // 24 hours for submitted tasks
};

function getTimeout(status, operationType) {
  // submitted tasks may take hours or days
  if (status === 'submitted') {
    return TIMEOUTS.submitted;
  }

  // working tasks should complete within 2 minutes
  if (status === 'working') {
    return TIMEOUTS.working;
  }

  // interactive operations wait for human input
  if (status === 'input-required') {
    return TIMEOUTS.interactive;
  }

  return TIMEOUTS.sync;
}

Human-in-the-Loop ワークフロー

設計原則

  1. デフォルトは任意 - 承認は実装ごとに設定
  2. 明確なメッセージ - 何を承認するかを明示
  3. 適切なタイムアウト - 人の入力で無期限にブロックしない
  4. 監査証跡 - 誰が何をいつ承認したか記録

承認パターン

async function handleApprovalWorkflow(response) {
  if (response.status === 'input-required' && needsApproval(response)) {
    // Show approval UI with context
    const approval = await showApprovalUI({
      title: "Campaign Approval Required",
      message: response.message,
      details: response,  // Task fields are at top level
      approver: getCurrentUser()
    });

    // Send approval decision
    const decision = {
      approved: approval.approved,
      notes: approval.notes,
      approver_id: approval.approver_id,
      timestamp: new Date().toISOString()
    };

    return sendFollowUp(response.context_id, decision);
  }
}

よくある承認トリガー

  • 予算閾値: $100K 超のキャンペーン
  • 新規広告主: 初回の購入者
  • センシティブコンテンツ: 特定業界や話題
  • 手動インベントリ: パブリッシャー承認が必要なプレミアム枠

進捗トラッキング

進捗更新

長時間処理では進捗情報が提供されることがあります:
{
  "status": "working",
  "message": "Processing creative assets...",
  "task_id": "task-456",
  "progress": 45,
  "step": "transcoding_video",
  "steps_completed": ["upload", "validation"],
  "steps_remaining": ["transcoding_video", "thumbnail_generation", "cdn_distribution"]
}

進捗表示

function displayProgress(response) {
  if (response.progress !== undefined) {
    updateProgressBar(response.progress);
  }

  if (response.step) {
    updateStatusText(`Step: ${response.step}`);
  }

  if (response.steps_completed) {
    updateStepsList(response.steps_completed, response.steps_remaining);
  }

  // Always show the message
  updateMessage(response.message);
}

プロトコル非依存パターン

これらのパターンは MCP/A2A どちらでも機能します。

確認フローを含む商品探索

async function discoverProducts(brief) {
  let response = await adcp.send({
    task: 'get_products',
    brief: brief
  });

  // 確認ループを処理
  while (response.status === 'input-required') {
    const moreInfo = await promptUser(response.message);
    response = await adcp.send({
      context_id: response.context_id,
      additional_info: moreInfo
    });
  }

  if (response.status === 'completed') {
    return response.products;  // Task fields are at top level
  } else if (response.status === 'failed') {
    throw new Error(response.message);
  }
}

Campaign Creation with Approval

async function createCampaign(packages, budget) {
  let response = await adcp.send({
    task: 'create_media_buy',
    packages: packages,
    total_budget: budget
  });

  // Handle approval if needed
  if (response.status === 'input-required') {
    const approved = await getApproval(response.message);
    if (!approved) {
      throw new Error('Campaign creation not approved');
    }

    response = await adcp.send({
      context_id: response.context_id,
      approved: true
    });
  }

  // 非同期作成の処理
  if (response.status === 'working') {
    response = await waitForCompletion(response);
  }

  if (response.status === 'completed') {
    return response.media_buy_id;  // Task fields are at top level
  } else {
    throw new Error(response.message);
  }
}

完了まで待機

async function waitForCompletion(initialResponse, options = {}) {
  const { maxWait = 120000, pollInterval = 5000 } = options;
  const startTime = Date.now();

  let response = initialResponse;

  while (response.status === 'working') {
    if (Date.now() - startTime > maxWait) {
      throw new Error('Operation timed out');
    }

    await sleep(pollInterval);

    response = await adcp.call('tasks/get', {
      task_id: response.task_id,
      include_result: true
    });
  }

  return response;
}

非同期前提の設計

状態を永続化する

非同期処理でメモリ状態に依存しない:
class AsyncOperationTracker {
  constructor(db) {
    this.db = db;
  }

  async startOperation(taskId, operationType, request) {
    await this.db.operations.insert({
      task_id: taskId,
      type: operationType,
      status: 'submitted',
      request: request,
      created_at: new Date(),
      updated_at: new Date()
    });
  }

  async updateStatus(taskId, status, result = null) {
    await this.db.operations.update(
      { task_id: taskId },
      {
        status: status,
        result: result,
        updated_at: new Date()
      }
    );
  }

  async getPendingOperations() {
    return this.db.operations.find({
      status: { $in: ['submitted', 'working', 'input-required'] }
    });
  }
}

再起動に耐える

オーケストレーター再起動後に追跡を再開:
async function onStartup() {
  const tracker = new AsyncOperationTracker(db);
  const pending = await tracker.getPendingOperations();

  for (const operation of pending) {
    // Check current status on server
    const response = await adcp.call('tasks/get', {
      task_id: operation.task_id,
      include_result: true
    });

    // Update local state
    await tracker.updateStatus(operation.task_id, response.status, response);

    // Resume polling if still pending
    if (['submitted', 'working'].includes(response.status)) {
      startPolling(operation.task_id);
    }
  }
}

ベストプラクティス

  1. 非同期前提で設計 - どの操作も時間がかかる前提
  2. 状態を永続化 - メモリだけに依存しない
  3. 再起動を考慮 - 起動時に追跡を再開
  4. タイムアウトを実装 - 無限に待たない
  5. 進捗を表示 - ユーザーに状況を伝える
  6. キャンセル対応 - 長時間処理をキャンセル可能に
  7. 監査証跡 - ステータス遷移をログ

次のステップ

  • Webhooks: ポーリングの代わりにプッシュ通知を使う場合は Webhooks
  • Task Lifecycle: ステータス処理の詳細は Task Lifecycle
  • Orchestrator Design: 本番パターンは Orchestrator Design