🐈

A2A を基礎から学ぶ (2) さくらのAI Engine / gpt-oss-120bでAgent間の通信を見る

に公開

https://zenn.dev/sakura_internet/articles/e29d64a9d211a4
この記事では前回に続いてA2Aを学んでいきます。

前回のおさらい

前回ではLLMを使わないシンプルなAgentを2つ作成し、Agentがお互いのAgentを知る仕組みを見ていきました。
今日はAgentがLLMと対話を行った後その結果をどのように別のAgentに引き渡しているのかを見ていきます。

さっそくやってみる

1. 基本準備

まずは前回の手順を終わらせておきます。
https://zenn.dev/sakura_internet/articles/e29d64a9d211a4

2. Agentスクリプトの入れ替え

agentA.js,agentB.jsを以下に入れ変えます。

agentA.js
// agentA-sakura.js - A2A Client (Planner Agent) for Sakura AI
// ========================================================
// さくらAI Engineを使うAgent Bに質問を投げるClient Agent
// ========================================================

import fetch from 'node-fetch';

const AGENT_B_URL = 'http://localhost:3001';

// ========================================================
// ユーティリティ: JSON整形表示
// ========================================================
function displayJson(label, data) {
  console.log(`\n${label}`);
  console.log('─'.repeat(60));
  console.log(JSON.stringify(data, null, 2));
  console.log('─'.repeat(60));
}

// ========================================================
// Agent Card の取得
// ========================================================
async function getAgentCard() {
  try {
    const response = await fetch(`${AGENT_B_URL}/.well-known/agent.json`);
    const card = await response.json();
    
    console.log('\n📄 Agent B のカード情報を取得:');
    console.log(`   名前: ${card.name}`);
    console.log(`   説明: ${card.description}`);
    console.log(`   プロバイダー: ${card.provider}`);
    console.log(`   モデル: ${card.model}`);
    console.log(`   利用可能なスキル:`);
    card.skills.forEach(skill => {
      console.log(`     - ${skill.name}: ${skill.description}`);
    });
    
    return card;
  } catch (error) {
    console.error('❌ Agent Card の取得に失敗:', error.message);
    throw error;
  }
}

// ========================================================
// タスクの作成
// ========================================================
async function createTask(instruction) {
  try {
    // リクエストボディを準備
    const requestBody = {
      jsonrpc: "2.0",
      method: "tasks.create",
      params: { instruction }
    };
    
    // 📤 リクエスト内容を表示
    displayJson('📤 Agent B へのリクエスト:', requestBody);
    
    const response = await fetch(`${AGENT_B_URL}/tasks`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(requestBody)
    });
    
    const data = await response.json();
    
    // 📥 レスポンス内容を表示
    displayJson('📥 Agent B からのレスポンス:', data);
    
    console.log('\n✓ Agent A: タスクを作成しました');
    console.log(`   タスクID: ${data.result.task_id}`);
    return data.result.task_id;
  } catch (error) {
    console.error('❌ タスク作成に失敗:', error.message);
    throw error;
  }
}

// ========================================================
// タスク状態のポーリング
// ========================================================
async function pollTaskStatus(taskId, maxAttempts = 30) {
  console.log('\n⏳ タスクの完了を待機中...\n');
  
  for (let i = 0; i < maxAttempts; i++) {
    try {
      const response = await fetch(`${AGENT_B_URL}/tasks/${taskId}`);
      const data = await response.json();
      
      const status = data.result.status;
      const statusEmoji = {
        'submitted': '📋',
        'working': '⚙️',
        'completed': '✅',
        'failed': '❌'
      };
      
      console.log(`   ${statusEmoji[status] || '❓'} [${i + 1}/${maxAttempts}] 状態: ${status}`);
      
      if (status === 'completed') {
        console.log('\n✓ Agent A: タスク完了を確認');
        
        // 📥 完了時の最終レスポンスを表示
        displayJson('📥 Agent B からの最終レスポンス(completed):', data);
        
        return data.result;
      }
      
      if (status === 'failed') {
        throw new Error('タスクが失敗しました: ' + (data.result.result?.error || '不明なエラー'));
      }
      
      // 1秒待機
      await new Promise(resolve => setTimeout(resolve, 1000));
      
    } catch (error) {
      console.error('❌ タスク状態取得に失敗:', error.message);
      throw error;
    }
  }
  
  throw new Error('タスク完了のタイムアウト');
}

// ========================================================
// メイン処理
// ========================================================
async function main() {
  console.log('\n' + '='.repeat(60));
  console.log('🚀 A2A プロトコル + さくらAI Engine デモ');
  console.log('='.repeat(60));
  
  try {
    // 1. Agent B のカード情報を取得
    const agentCard = await getAgentCard();
    
    // 2. 質問を準備
    const questions = [
      "A2Aプロトコルとは何ですか?簡潔に説明してください。",
      // 必要に応じて質問を追加
      // "さくらのAI Engineの特徴は何ですか?",
      // "JavaScriptとTypeScriptの違いを教えてください。"
    ];
    
    // 3. 各質問をAgent Bに投げる
    for (const question of questions) {
      console.log('\n' + '-'.repeat(60));
      console.log(`📝 質問: "${question}"`);
      console.log('-'.repeat(60));
      
      // タスク作成
      const taskId = await createTask(question);
      
      // 完了を待つ
      const result = await pollTaskStatus(taskId);
      
      // 結果を表示
      console.log('='.repeat(60));
      console.log('🎯 AI からの回答:');
      console.log('='.repeat(60));
      console.log(result.result.answer);
      console.log('\n📊 メタ情報:');
      console.log(`   モデル: ${result.result.model}`);
      console.log(`   使用トークン: ${JSON.stringify(result.result.usage)}`);
      console.log(`   タイムスタンプ: ${result.result.timestamp}`);
      console.log('='.repeat(60));
    }
    
    console.log('\n✅ すべての質問が完了しました!\n');
    
  } catch (error) {
    console.error('\n❌ エラーが発生しました:', error.message);
    console.error(error.stack);
    process.exit(1);
  }
}

// 実行
main();
agentB.js
// agentB-sakura.js - A2A Server (Worker Agent) with Sakura AI Engine
// ========================================================
// さくらのAI Engineを使ってLLMで応答するWorkerエージェント
// ========================================================

import express from 'express';
import { v4 as uuidv4 } from 'uuid';
import fetch from 'node-fetch';

const app = express();
app.use(express.json());

const PORT = 3001;

// さくらAI Engineの設定
const SAKURA_API_URL = 'https://api.ai.sakura.ad.jp/v1/chat/completions';
const SAKURA_API_KEY = 'xxf3e60b-7c0a-4685-b5eb-148fbe845cc5:mFhWGnNfWmfvy4psmZgWKQ4EkrzEy5MQxu9vuyV5';
const SAKURA_MODEL = 'gpt-oss-120b';

// タスクの状態を保存(メモリ内)
const tasks = new Map();

// ========================================================
// Agent Card - エージェントのメタデータ
// ========================================================
const agentCard = {
  name: "SakuraAIAgent",
  version: "1.0.0",
  description: "さくらのAI Engineを使って質問に答えるエージェント",
  capabilities: ["text", "llm"],
  skills: [
    {
      name: "answerQuestion",
      description: "LLMを使って質問に答えます"
    },
    {
      name: "generateText",
      description: "指示に基づいてテキストを生成します"
    }
  ],
  endpoint: `http://localhost:${PORT}`,
  provider: "Sakura AI Engine",
  model: SAKURA_MODEL
};

// ========================================================
// GET /.well-known/agent.json - エージェントカードの公開
// ========================================================
app.get('/.well-known/agent.json', (req, res) => {
  res.json(agentCard);
});

// ========================================================
// さくらAI Engineを呼び出す関数
// ========================================================
async function callSakuraAI(userMessage, systemPrompt = "あなたは親切なアシスタントです。") {
  try {
    // メッセージのバリデーション
    if (!userMessage || typeof userMessage !== 'string') {
      throw new Error('userMessageが不正です');
    }
    
    const messagePreview = userMessage.length > 50 
      ? userMessage.substring(0, 50) + "..."
      : userMessage;
    
    console.log(`🤖 さくらAI Engine呼び出し: "${messagePreview}"`);
    
    const response = await fetch(SAKURA_API_URL, {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'Authorization': `Bearer ${SAKURA_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        model: SAKURA_MODEL,
        messages: [
          { role: "system", content: systemPrompt },
          { role: "user", content: userMessage }
        ],
        temperature: 0.7,
        max_tokens: 500,
        stream: false
      })
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`Sakura AI API error: ${response.status} - ${errorText}`);
    }

    const data = await response.json();
    const aiResponse = data.choices[0].message.content;
    
    const responsePreview = aiResponse && aiResponse.length > 50
      ? aiResponse.substring(0, 50) + "..."
      : aiResponse || "(応答なし)";
    
    console.log(`✅ AI応答取得: "${responsePreview}"`);
    
    return {
      content: aiResponse,
      model: data.model,
      usage: data.usage
    };
    
  } catch (error) {
    console.error('❌ さくらAI Engine呼び出しエラー:', error.message);
    throw error;
  }
}

// ========================================================
// POST /tasks - タスクの作成
// ========================================================
app.post('/tasks', (req, res) => {
  // JSON-RPC形式のparamsから取得
  const { instruction } = req.body.params || req.body;
  
  const taskId = uuidv4();
  const task = {
    id: taskId,
    status: "submitted",
    instruction,
    createdAt: new Date().toISOString(),
    result: null
  };
  
  tasks.set(taskId, task);
  
  console.log(`\n📥 Agent B: タスク受信 [${taskId}]`);
  console.log(`   指示: "${instruction || '(指示なし)'}"`);
  
  // タスクを非同期で処理
  setTimeout(() => {
    processTask(taskId);
  }, 100);
  
  res.status(201).json({
    jsonrpc: "2.0",
    id: taskId,
    result: {
      task_id: taskId,
      status: "submitted"
    }
  });
});

// ========================================================
// GET /tasks/:taskId - タスク状態の取得
// ========================================================
app.get('/tasks/:taskId', (req, res) => {
  const { taskId } = req.params;
  const task = tasks.get(taskId);
  
  if (!task) {
    return res.status(404).json({
      jsonrpc: "2.0",
      error: {
        code: -32001,
        message: "Task not found"
      }
    });
  }
  
  res.json({
    jsonrpc: "2.0",
    id: taskId,
    result: {
      task_id: task.id,
      status: task.status,
      result: task.result
    }
  });
});

// ========================================================
// タスク処理(さくらAI Engineを使用)
// ========================================================
async function processTask(taskId) {
  const task = tasks.get(taskId);
  if (!task) return;
  
  try {
    // 状態を "working" に更新
    task.status = "working";
    console.log(`⚙️  Agent B: タスク処理中 [${taskId}]`);
    
    // さくらAI Engineで回答を生成
    const aiResult = await callSakuraAI(
      task.instruction,
      "あなたは親切で正確なAIアシスタントです。質問に対して簡潔で分かりやすい回答を提供してください。"
    );
    
    // 状態を "completed" に更新
    task.status = "completed";
    task.result = {
      answer: aiResult.content,
      model: aiResult.model,
      usage: aiResult.usage,
      timestamp: new Date().toISOString()
    };
    
    console.log(`✅ Agent B: タスク完了 [${taskId}]`);
    
    const answerPreview = aiResult.content && aiResult.content.length > 100
      ? aiResult.content.substring(0, 100) + "..."
      : aiResult.content || "(応答なし)";
    
    console.log(`   回答: "${answerPreview}"`);
    
  } catch (error) {
    // エラーが発生した場合
    task.status = "failed";
    task.result = {
      error: error.message,
      timestamp: new Date().toISOString()
    };
    
    console.error(`❌ Agent B: タスク失敗 [${taskId}]`, error.message);
  }
}

// ========================================================
// サーバー起動
// ========================================================
app.listen(PORT, () => {
  console.log(`\n${'='.repeat(60)}`);
  console.log(`🚀 Agent B (Sakura AI Worker) が起動しました`);
  console.log(`${'='.repeat(60)}`);
  console.log(`📍 エンドポイント: http://localhost:${PORT}`);
  console.log(`📄 Agent Card: http://localhost:${PORT}/.well-known/agent.json`);
  console.log(`🤖 LLMプロバイダー: Sakura AI Engine`);
  console.log(`🔧 モデル: ${SAKURA_MODEL}`);
  console.log(`${'='.repeat(60)}\n`);
});

前回同様に起動を行うとAgentA.js側で以下の出力が行われます。

npm run agent-a

> a2a-simple-demo@1.0.0 agent-a
> node agentA.js


============================================================
🚀 A2A プロトコル + さくらAI Engine デモ
============================================================

📄 Agent B のカード情報を取得:
   名前: SakuraAIAgent
   説明: さくらのAI Engineを使って質問に答えるエージェント
   プロバイダー: Sakura AI Engine
   モデル: gpt-oss-120b
   利用可能なスキル:
     - answerQuestion: LLMを使って質問に答えます
     - generateText: 指示に基づいてテキストを生成します

------------------------------------------------------------
📝 質問: "A2Aプロトコルとは何ですか?簡潔に説明してください。"
------------------------------------------------------------

📤 Agent B へのリクエスト:
────────────────────────────────────────────────────────────
{
  "jsonrpc": "2.0",
  "method": "tasks.create",
  "params": {
    "instruction": "A2Aプロトコルとは何ですか?簡潔に説明してください。"
  }
}
────────────────────────────────────────────────────────────

📥 Agent B からのレスポンス:
────────────────────────────────────────────────────────────
{
  "jsonrpc": "2.0",
  "id": "ed14c736-5a7b-4e9b-a4e4-047975d7b320",
  "result": {
    "task_id": "ed14c736-5a7b-4e9b-a4e4-047975d7b320",
    "status": "submitted"
  }
}
────────────────────────────────────────────────────────────

✓ Agent A: タスクを作成しました
   タスクID: ed14c736-5a7b-4e9b-a4e4-047975d7b320

⏳ タスクの完了を待機中...

   📋 [1/30] 状態: submitted
   ⚙️ [2/30] 状態: working
   ⚙️ [3/30] 状態: working
   ⚙️ [4/30] 状態: working
   ✅ [5/30] 状態: completed

✓ Agent A: タスク完了を確認

📥 Agent B からの最終レスポンス(completed):
────────────────────────────────────────────────────────────
{
  "jsonrpc": "2.0",
  "id": "ed14c736-5a7b-4e9b-a4e4-047975d7b320",
  "result": {
    "task_id": "ed14c736-5a7b-4e9b-a4e4-047975d7b320",
    "status": "completed",
    "result": {
      "answer": "**A2A(Application‑to‑Application)プロトコル**は、異なるアプリケーション同士が直接データや機能をやり取りするための通信規約・ 方式のことです。主な特徴は以下の通りです。\n\n| 項目 | 内容 |\n|------|------|\n| **目的** | アプリケーション間で自動的に情報交換・連携を行う |\n| **利用シーン** | 企業システム統合、B2B取引、マイクロサービス間通信、IoTデバイス間連携など |\n| **代表的なプロトコル例** | - **REST/HTTP**(JSONやXMLでデータをやり取り)<br>- **SOAP**(XMLベースのWebサービス)<br>- **gRPC**(バイナリ形式で高速通信)<br>- **MQTT**(軽量メッセージング、IoT向け) |\n| **メリット** | - システム間の疎結合が実現できる<br>- 標準化されたインターフェースで拡張性が高い<br>- 自動化・リアルタイム連携 が可能 |\n| **課題** | - セキュリティ(認証・暗号化)の確保が必要<br>- 異なるデータフォーマットやバージョン管理の調整が必要 |\n\n要するに、A2A プロトコルは「アプリケーション同士が相互に通信し、機能やデータを共有するための標準的な手段」だと言えます。",
      "model": "gpt-oss-120b",
      "usage": {
        "prompt_tokens": 124,
        "total_tokens": 598,
        "completion_tokens": 474,
        "prompt_tokens_details": null
      },
      "timestamp": "2025-11-20T05:31:02.048Z"
    }
  }
}
────────────────────────────────────────────────────────────
============================================================
🎯 AI からの回答:
============================================================
**A2A(Application‑to‑Application)プロトコル**は、異なるアプリケーション同士が直接データや機能をやり取りするための通信規約・方式のことです。主な特徴は以下の通りです。

| 項目 | 内容 |
|------|------|
| **目的** | アプリケーション間で自動的に情報交換・連携を行う |
| **利用シーン** | 企業システム統合、B2B取引、マイクロサービス間通信、IoTデバイス間連携など |
| **代表的なプロトコル例** | - **REST/HTTP**(JSONやXMLでデータをやり取り)<br>- **SOAP**(XMLベースのWebサービス)<br>- **gRPC**(バイナリ形式で高速通信)<br>- **MQTT**(軽量メッセージング、IoT向け) |
| **メリット** | - システム間の疎結合が実現できる<br>- 標準化されたインターフェースで拡張性が高い<br>- 自動化・リアルタイム連携が可能 |        
| **課題** | - セキュリティ(認証・暗号化)の確保が必要<br>- 異なるデータフォーマットやバージョン管理の調整が必要 |

要するに、A2Aプロトコルは「アプリケーション同士が相互に通信し、機能やデータを共有するための標準的な手段」だと言えます。

📊 メタ情報:
   モデル: gpt-oss-120b
   使用トークン: {"prompt_tokens":124,"total_tokens":598,"completion_tokens":474,"prompt_tokens_details":null}
   タイムスタンプ: 2025-11-20T05:31:02.048Z
============================================================

✅ すべての質問が完了しました!

3. AgentA→AgentBのリクエストとレスポンス

Agent間の通信はJSON-RPCで行われておるため、内容はJSONです。
まずAgentAからAgentBに以下のリクエストが出されています。

📤 Agent B へのリクエスト:
────────────────────────────────────────────────────────────
{
  "jsonrpc": "2.0",
  "method": "tasks.create",
  "params": {
    "instruction": "A2Aプロトコルとは何ですか?簡潔に説明してください。"
  }
}
────────────────────────────────────────────────────────────

その後AgentBから着手開始のレスポンスが戻ります。

📥 Agent B からのレスポンス:
────────────────────────────────────────────────────────────
{
  "jsonrpc": "2.0",
  "id": "ed14c736-5a7b-4e9b-a4e4-047975d7b320",
  "result": {
    "task_id": "ed14c736-5a7b-4e9b-a4e4-047975d7b320",
    "status": "submitted"
  }
}
────────────────────────────────────────────────────────────

AgentBが作業を行っている間AgentAはポーリングでそのジョブステータスを確認しています。

   📋 [1/30] 状態: submitted
   ⚙️ [2/30] 状態: working
   ⚙️ [3/30] 状態: working
   ⚙️ [4/30] 状態: working
   ✅ [5/30] 状態: completed

最後にレスポンスがAgentBから戻ります。

📥 Agent B からの最終レスポンス(completed):
────────────────────────────────────────────────────────────
{
  "jsonrpc": "2.0",
  "id": "ed14c736-5a7b-4e9b-a4e4-047975d7b320",
  "result": {
    "task_id": "ed14c736-5a7b-4e9b-a4e4-047975d7b320",
    "status": "completed",
    "result": {
      "answer": "**A2A(Application‑to‑Application)プロトコル**は、異なるアプリケーション同士が直接データや機能をやり取りするための通信規約・ 方式のことです。主な特徴は以下の通りです。\n\n| 項目 | 内容 |\n|------|------|\n| **目的** | アプリケーション間で自動的に情報交換・連携を行う |\n| **利用シーン** | 企業システム統合、B2B取引、マイクロサービス間通信、IoTデバイス間連携など |\n| **代表的なプロトコル例** | - **REST/HTTP**(JSONやXMLでデータをやり取り)<br>- **SOAP**(XMLベースのWebサービス)<br>- **gRPC**(バイナリ形式で高速通信)<br>- **MQTT**(軽量メッセージング、IoT向け) |\n| **メリット** | - システム間の疎結合が実現できる<br>- 標準化されたインターフェースで拡張性が高い<br>- 自動化・リアルタイム連携 が可能 |\n| **課題** | - セキュリティ(認証・暗号化)の確保が必要<br>- 異なるデータフォーマットやバージョン管理の調整が必要 |\n\n要するに、A2A プロトコルは「アプリケーション同士が相互に通信し、機能やデータを共有するための標準的な手段」だと言えます。",
      "model": "gpt-oss-120b",
      "usage": {
        "prompt_tokens": 124,
        "total_tokens": 598,
        "completion_tokens": 474,
        "prompt_tokens_details": null
      },
      "timestamp": "2025-11-20T05:31:02.048Z"
    }
  }
}
────────────────────────────────────────────────────────────

次回予告

今のサンプルではAgentAがAgentBを明示的に呼び出す部分がコーディング化されています。本来複数のAgentをどう呼び出すかLLMが判断する、というのが商用環境におけるよくあるパターンですので、そのサンプルを作ってみていきたいと思います。

さくらインターネット株式会社

Discussion