GoogleのA2A CodelabエージェントをA2A SDK v1.0に移行してCloud Runで動かす
GoogleのA2A CodelabにあるCrewAI製のバーガーエージェントをa2a-sdk v0.2からv1.0に移行し、Cloud Runにデプロイして、TypeScriptクライアントから接続します。

目次
はじめに
AIMockの記事では、バイヤーエージェントのテストをモックのA2Aセラーエージェントに対して実行しました。この記事では、同じTypeScriptクライアントを実際のエージェントに接続します。接続先は、GoogleのA2A Codelabにあるバーガーエージェントをa2a-sdk v1.0に移行し、Cloud Runにデプロイしたものです。
A2Aエコシステムの現状
A2Aプロトコルの調査で、公開されていて自由にインタラクトできるエージェントを探してみました。
見つかったもの
- Hello World Agent:
https://hello-world-gxfr.onrender.com/.well-known/agent.json。最初の公開A2Aエージェント。エコー応答のみですが、クライアントの動作確認には使えます - A2A Registry Playground: a2a-registry.org/playground。登録されたエージェントと対話できるプレイグラウンド
- Google Codelab: Purchasing Concierge。Cloud Runに2つのエージェントをデプロイし、A2Aで通信させるハンズオン。無料枠で実行可能
デモエージェントはあっても、本番エンドポイントはない
| 存在するもの | まだないもの |
|---|---|
| Hello World / エコーエージェント | 実企業が公開する本番用A2Aエンドポイント |
| Google Codelabのデモ | Stripe、Twilioなどが公開するA2A API |
| コミュニティのトイエージェント | 実用的な機能を持つエージェントマーケットプレイス |
A2Aは150以上の組織が参加し、GitHubスターも22,000以上ですが、公開されていて自由にインタラクトできる実用的なエージェントはほぼ存在しません。エンタープライズでのA2A導入(Azure AI Foundry、Bedrock AgentCore)はほとんど認証の背後にあります。
Google Codelabの実A2Aエージェントをローカルで動かす
デモのテストでは、http.createServer で自作したモックのA2Aセラーエージェントを使いました。次のステップとして、Google CodelabのPurchasing Conciergeに含まれる実A2Aエージェントをローカルで動かしてみました。
リポジトリのクローンと調整
git clone https://github.com/alphinside/purchasing-concierge-intro-a2a-codelab-starter.gitcd purchasing-concierge-intro-a2a-codelab-starter/remote_seller_agents/burger_agentこのburger agentはCrewAI + LiteLLM + Vertex AI Geminiで構築されたA2Aセラーエージェントです。ユーザーからのバーガー注文を受け付けて処理します。
スターターリポジトリを動かすには、以下の変更が必要でした。
1. Python 3.13への更新
requires-python = ">=3.13"FROM python:3.13-slim2. litellm の明示的な依存追加
agent.py が import litellm しているのに、pyproject.toml の依存には含まれていませんでした。以前はCrewAIの推移的依存として入っていたようですが、新しいバージョンでは外れています。
uv add litellma2a-sdk v0.2 → v1.0 マイグレーション
元のコードは a2a-sdk v0.2系向けに書かれていますが、>=0.2.16 の指定で v1.1.2 がインストールされ、破壊的変更によりインポートエラーが発生します。v1.0に合わせてコードを修正しました。
v1.0で変更が多い理由
Protobuf(Protocol Buffers)は、Googleが開発したメッセージの型を定義するための形式です。.proto スキーマに一度記述すると、ジェネレーターが各言語向けのクラスを生成します。同じメッセージを、gRPCでバイナリとして送ることも、JSONとして送ることもできます。
v1.0でA2Aは、a2a.proto をgRPC専用のファイルから、JSON-RPC・REST・gRPCの3つのバインディングに共通する規範的な仕様の基準に格上げしました。a2a-sdk v1.0もこれに合わせて、Pydanticモデルを a2a.proto から生成した型に置き換えています。v1.0の破壊的変更の多くは、この1つの決定に由来します。
すべての利用側が同じprotoから派生するので、Python SDKでも通信上でも同じ名前が現れます。
| 変更 | 由来 |
|---|---|
Part(root=TextPart(text="...")) → Part(text="...") | Part は内容を oneof で定義したprotoメッセージなので、フィールドを直接設定する |
"user" → "ROLE_USER" | ProtoJSONに準拠するため、列挙値が SCREAMING_SNAKE_CASE になった |
message/send → SendMessage | メソッド名をprotoの rpc SendMessage に合わせている |
url → supported_interfaces | バインディングごとに AgentInterface として宣言する |
v1.0向けのコード変更
Codelabは a2a-sdk>=0.2.16 を指定しており、これはA2A v0.2の実装です。この仕様の送信メソッドは message/send です。主な変更点:
| 項目 | v0.2 | v1.0 |
|---|---|---|
| サーバー構築 | A2AStarletteApplication クラス | create_jsonrpc_routes() / create_agent_card_routes() ルートファクトリ |
| AgentCard | url フィールド | supported_interfaces リスト(AgentInterface) |
| RequestHandler | agent_card 不要 | agent_card が必須パラメータ |
| Part構築 | Part(root=TextPart(text="...")) | Part(text="...")(Protobufベース) |
| JSON-RPCメソッド | message/send | SendMessage |
| エージェントカードパス | /.well-known/agent.json | /.well-known/agent-card.json |
| プロトコルバージョン | なし(暗黙的に0.3) | A2A-Version: 1.0 ヘッダーが必要 |
| エラー型 | ServerError | UnsupportedOperationError を直接使用 |
__main__.py の変更(サーバー構築):
from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routesfrom a2a.types import AgentCard, AgentInterface, AgentCapabilities, AgentSkillfrom starlette.applications import Starlette
agent_card = AgentCard( name="burger_seller_agent", description="Helps with creating burger orders", version="1.0.0", capabilities=AgentCapabilities(streaming=True), supported_interfaces=[ AgentInterface(protocol_binding="JSONRPC", url=agent_host_url) ], skills=[skill],)
request_handler = DefaultRequestHandler( agent_executor=BurgerSellerAgentExecutor(), task_store=InMemoryTaskStore(), agent_card=agent_card, # v1.0で必須に)
routes = []routes.extend(create_agent_card_routes(agent_card))routes.extend(create_jsonrpc_routes(request_handler, "/"))
app = Starlette(routes=routes)uvicorn.run(app, host=host, port=port)agent_executor.py の変更(Protobufベースの型):
from a2a.types import Message, Part, Role, Taskfrom a2a.utils.errors import UnsupportedOperationError
class BurgerSellerAgentExecutor(AgentExecutor): async def execute(self, context, event_queue): query = context.message.parts[0].text if context.message else "" result = await self.agent.invoke(query, context.context_id)
# v1.0: Part(text="...") で直接構築(TextPartラッパー不要) await event_queue.enqueue_event( Message( role=Role.ROLE_AGENT, parts=[Part(text=str(result))], message_id=context.task_id or "msg", context_id=context.context_id, ) )agent.py の変更(async化):
CrewAIの kickoff() は同期メソッドですが、a2a-sdk v1.0ではリクエストハンドラが非同期コンテキストから呼ばれます。同期メソッドを非同期コンテキスト内で呼ぶと Agent execution was invoked synchronously from within a running event loop エラーが発生します。
def invoke(self, query, sessionId) -> str:async def invoke(self, query, sessionId) -> str: # ... response = crew.kickoff(inputs) response = await crew.kickoff_async(inputs) return response動作確認
# Vertex AI APIの有効化が必要gcloud services enable aiplatform.googleapis.com
# ローカルで起動uv run . --host 127.0.0.1 --port 8080エージェントカードの確認:
$ curl http://127.0.0.1:8080/.well-known/agent-card.json | jq .name"burger_seller_agent"A2A v1.0プロトコルでのメッセージ送信:
$ curl -X POST http://127.0.0.1:8080/ \ -H "Content-Type: application/json" \ -H "A2A-Version: 1.0" \ -d '{"jsonrpc":"2.0","id":"1","method":"SendMessage", "params":{"message":{"role":"ROLE_USER", "parts":[{"text":"What burgers do you have?"}], "messageId":"m1","contextId":"ctx1"}}}'レスポンス:
{ "result": { "message": { "role": "ROLE_AGENT", "parts": [{ "text": "We have Classic Cheeseburger for IDR 85K, Double Cheeseburger for IDR 110K, Spicy Chicken Burger for IDR 80K, and Spicy Cajun Burger for IDR 85K. Do any of these sound good?" }] } }, "id": "1", "jsonrpc": "2.0"}Vertex AI Gemini → CrewAI → A2A v1.0プロトコルの全経路が動作しています。
Cloud Runへのデプロイ
ローカルで動作確認ができたら、Cloud Runにデプロイします。このburger agentはStarlette ASGIサーバー(uvicornで起動)なので、Cloud FunctionsではなくCloud Runが適しています。このエージェントはStarletteの複数ルート(/.well-known/agent-card.json、/ のJSON-RPCエンドポイント)を提供します。
gcloud run deploy burger-agent \ --source . \ --port 8080 \ --allow-unauthenticated \ --region us-central1 \ --min-instances 0 \ --max-instances 1 \ --memory 1Gi--source . でDockerfileを使ったビルドがCloud Build上で実行され、Artifact Registryにイメージが保存された後、Cloud Runにデプロイされます。
デプロイ後、HOST_OVERRIDE にサービスのURLを設定し、エージェントカードの url フィールドがCloud Runを指すようにします。
gcloud run services update burger-agent \ --region us-central1 \ --set-env-vars HOST_OVERRIDE=https://burger-agent-XXXXXXXXXX.us-central1.run.app動作確認:
$ curl https://burger-agent-XXXXXXXXXX.us-central1.run.app/.well-known/agent-card.json | jq .name"burger_seller_agent"
$ curl -X POST https://burger-agent-XXXXXXXXXX.us-central1.run.app/ \ -H "Content-Type: application/json" \ -H "A2A-Version: 1.0" \ -d '{"jsonrpc":"2.0","id":"1","method":"SendMessage", "params":{"message":{"role":"ROLE_USER", "parts":[{"text":"What burgers do you have?"}], "messageId":"m1","contextId":"ctx1"}}}'{ "result": { "message": { "role": "ROLE_AGENT", "parts": [{ "text": "We have Classic Cheeseburger for IDR 85K, Double Cheeseburger for IDR 110K, Spicy Chicken Burger for IDR 80K, and Spicy Cajun Burger for IDR 85K. What would you like to order?" }] } }}コストに関する注意: --min-instances 0 を指定しているので、アイドル時はサービスが0までスケールダウンし、使われていないデモにコストはかかりません。その代わりコールドスタートに10〜15秒かかります。デモやテスト用途であれば問題ありません。
TypeScriptバイヤーエージェントをA2A v1.0に対応させる
Cloud Runにデプロイした実セラーエージェントに接続するため、TypeScript側のA2Aクライアントもv1.0プロトコルに対応させました。
A2Aクライアントの変更
export class A2AClient { // v1.0: agent-card.json(旧: agent.json) async discover(): Promise<AgentCard> { const url = `${this.agentUrl}/.well-known/agent-card.json`; const res = await fetch(url); this.card = await res.json(); return this.card; }
// v1.0: SendMessage + A2A-Version ヘッダー async sendMessage(text: string, contextId?: string): Promise<A2AMessage> { const rpcRequest = { jsonrpc: "2.0", id: messageId, method: "SendMessage", // 旧: "tasks/send" params: { message: { role: "ROLE_USER", // 旧: "user" parts: [{ text }], // 旧: { type: "text", text } messageId, contextId, }, }, };
const res = await fetch(this.agentUrl, { method: "POST", headers: { "Content-Type": "application/json", "A2A-Version": "1.0", // v1.0で必須 }, body: JSON.stringify(rpcRequest), });
const rpcResponse = await res.json(); return rpcResponse.result.message; // 旧: result(タスクオブジェクト) }}v0.1 → v1.0 変更点まとめ(TypeScript側)
私のTypeScriptクライアントはSDKより古い版に沿っていました。tasks/send、{ type: "text" }、agent.json はA2A v0.1のものです。
| 項目 | v0.1 | v1.0 |
|---|---|---|
| エージェントカードパス | /.well-known/agent.json | /.well-known/agent-card.json |
| JSON-RPCメソッド | tasks/send | SendMessage |
| ロール値 | "user" / "agent" | "ROLE_USER" / "ROLE_AGENT" |
| パート構造 | { type: "text", text: "..." } | { text: "..." } |
| ヘッダー | なし | A2A-Version: 1.0 |
| レスポンス | result = Task(messagesの配列) | result.message = 単一Message |
実Cloud Runエージェントとの接続テスト
TypeScript A2Aクライアントから実Cloud Runバーガーエージェントへの接続を確認しました。
const client = new A2AClient("https://burger-agent-XXXXXXXXXX.us-central1.run.app");
const card = await client.discover();// → Name: burger_seller_agent, Skills: ["create_burger_order"]
const reply = await client.sendMessage("What burgers do you have?");// → role: ROLE_AGENT// → "We have Classic Cheeseburger for IDR 85K, Double Cheeseburger for IDR 110K, ..."モックテストと実エージェントが同じA2Aクライアントコード、同じv1.0プロトコルで動作します。テスト時はAIMockのモックサーバー、本番では実Cloud Runエージェントに向き先を変えるだけです。
まとめ
A2AStarletteApplicationは廃止。ルートファクトリ(create_jsonrpc_routes/create_agent_card_routes)を使って標準のStarletteアプリに組み込む- 型システムがPydanticからProtobufベースに変更。
Part(text="...")のように直接構築する - エージェントカードのパスが
agent.json→agent-card.jsonに変更 - JSON-RPCメソッド名が
message/send→SendMessageに変更、A2A-Version: 1.0ヘッダーが必須 - A2AエージェントはStarlette ASGIサーバーのため、Cloud Functions(単一関数ハンドラモデル)ではなくCloud Run(Dockerコンテナ)でデプロイする
- Cloud Runでは
HOST_OVERRIDE環境変数でエージェントカードの公開URLを設定する - TypeScript側のA2Aクライアントも同様にv1.0対応が必要(エンドポイントパス、メソッド名、ロール値、ヘッダーの変更)
- モックと実エージェントで同じクライアントコード・同じプロトコルが使えるため、向き先(URL)を変えるだけでテスト↔本番を切り替えられる




