GoogleのA2A CodelabエージェントをA2A SDK v1.0に移行してCloud Runで動かす

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

白いカードに青のAgent2AgentのA-2-A円形マークとロゴタイプ
目次

はじめに

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.git
cd purchasing-concierge-intro-a2a-codelab-starter/remote_seller_agents/burger_agent

このburger agentはCrewAI + LiteLLM + Vertex AI Geminiで構築されたA2Aセラーエージェントです。ユーザーからのバーガー注文を受け付けて処理します。

スターターリポジトリを動かすには、以下の変更が必要でした。

1. Python 3.13への更新

pyproject.toml
requires-python = ">=3.13"
Dockerfile
FROM python:3.13-slim

2. litellm の明示的な依存追加

agent.py が import litellm しているのに、pyproject.toml の依存には含まれていませんでした。以前はCrewAIの推移的依存として入っていたようですが、新しいバージョンでは外れています。

ターミナルウィンドウ
uv add litellm

a2a-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つの決定に由来します。

すべての利用側の元になる a2a.proto上部に仕様の基準である a2a.proto があり、oneof content を持つ message Part、ROLE_USER を含む enum Role、rpc SendMessage の 3 行が並ぶ。そこから 3 つのカードに分かれる。Python の a2a-sdk は生成されたクラスを使い、Part(text="...") と Role.ROLE_AGENT で書く。JSON-RPC バインディングは HTTP 上の ProtoJSON で、メソッド SendMessage とロール ROLE_USER を送る。gRPC バインディングは HTTP/2 上のバイナリで rpc SendMessage を使う。REST も同じメッセージを JSON で運ぶ。a2a.protoすべてのバインディングに共通する仕様の基準message Part { oneof content … }enum Role { ROLE_USER = 1; … }rpc SendMessage(…)型を生成バインディングを定義a2a-sdk(Python)生成されたクラスPart(text="...")Role.ROLE_AGENTJSON-RPCHTTP 上の ProtoJSON"method": "SendMessage""role": "ROLE_USER"gRPCHTTP/2 上のバイナリrpc SendMessageROLE_USER = 1REST も同じメッセージを JSON で運ぶ。

すべての利用側が同じ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.2v1.0
サーバー構築A2AStarletteApplication クラスcreate_jsonrpc_routes() / create_agent_card_routes() ルートファクトリ
AgentCardurl フィールドsupported_interfaces リスト(AgentInterface)
RequestHandleragent_card 不要agent_card が必須パラメータ
Part構築Part(root=TextPart(text="..."))Part(text="...")(Protobufベース)
JSON-RPCメソッドmessage/sendSendMessage
エージェントカードパス/.well-known/agent.json/.well-known/agent-card.json
プロトコルバージョンなし(暗黙的に0.3)A2A-Version: 1.0 ヘッダーが必要
エラー型ServerErrorUnsupportedOperationError を直接使用

__main__.py の変更(サーバー構築):

__main__.py
from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
from a2a.types import AgentCard, AgentInterface, AgentCapabilities, AgentSkill
from 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ベースの型):

agent_executor.py
from a2a.types import Message, Part, Role, Task
from 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 エラーが発生します。

agent.py
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クライアントの変更

src/buyer-agent/a2a-client.ts
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.1v1.0
エージェントカードパス/.well-known/agent.json/.well-known/agent-card.json
JSON-RPCメソッドtasks/sendSendMessage
ロール値"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)を変えるだけでテスト↔本番を切り替えられる

参考リンク

この記事をシェア