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

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

- Source: https://oharu121.com/ja/blog/a2a-sdk-v1-migration-crewai-agent-cloud-run-typescript-client/
- Published: 2026-11-01T12:00:00+09:00
- Tags: A2A, CrewAI, Python, Google Cloud, TypeScript, AIエージェント

---
## はじめに

[AIMockの記事](/ja/blog/aimock-deterministic-testing-ai-agent-llm-vectordb-a2a/)では、バイヤーエージェントのテストをモックの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](https://www.a2a-registry.org/playground)。登録されたエージェントと対話できるプレイグラウンド
- **Google Codelab**: [Purchasing Concierge](https://codelabs.developers.google.com/intro-a2a-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](https://codelabs.developers.google.com/intro-a2a-purchasing-concierge)に含まれる実A2Aエージェントをローカルで動かしてみました。

### リポジトリのクローンと調整

```bash
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への更新**

```toml title="pyproject.toml"
requires-python = ">=3.13"
```

```dockerfile title="Dockerfile"
FROM python:3.13-slim
```

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

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

```bash
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つのバインディングに共通する[規範的な仕様の基準](https://github.com/a2aproject/A2A/blob/main/docs/whats-new-v1.md)に格上げしました。a2a-sdk v1.0もこれに合わせて、Pydanticモデルを `a2a.proto` から生成した型に置き換えています。v1.0の破壊的変更の多くは、この1つの決定に由来します。

*Figure — ProtoSourceOfTruth: すべての利用側が同じ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](https://github.com/a2aproject/A2A/tree/v0.2.5/specification)の実装です。この仕様の送信メソッドは `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` の変更（サーバー構築）:**

```python title="__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ベースの型）:**

```python title="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` エラーが発生します。

```python title="agent.py" del={1,4} ins={2,5}
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
```

### 動作確認

```bash
# Vertex AI APIの有効化が必要
gcloud services enable aiplatform.googleapis.com

# ローカルで起動
uv run . --host 127.0.0.1 --port 8080
```

エージェントカードの確認:

```bash
$ curl http://127.0.0.1:8080/.well-known/agent-card.json | jq .name
"burger_seller_agent"
```

A2A v1.0プロトコルでのメッセージ送信:

```bash
$ 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"}}}'
```

レスポンス:

```json
{
  "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エンドポイント）を提供します。

> **ちなみに**
>
> StarletteはPythonのASGI Webフレームワークで、uvicornはStarletteアプリを実行するサーバーです。2つを組み合わせると、ポートで待ち受け続けるHTTPサーバーになります。Cloud Runが実行するのはまさにこのようなコンテナです。一方、Cloud Functionsはリクエストごとに単一の関数を呼び出すだけなので、独自のルートを持つサーバー全体は収まりません。

```bash
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を指すようにします。

```bash
gcloud run services update burger-agent \
  --region us-central1 \
  --set-env-vars HOST_OVERRIDE=https://burger-agent-XXXXXXXXXX.us-central1.run.app
```

動作確認:

```bash
$ 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"}}}'
```

```json
{
  "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クライアントの変更

```ts title="src/buyer-agent/a2a-client.ts" ins={4,15,18,19,30,36}
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](https://github.com/a2aproject/A2A/tree/v0.1.0/specification)のものです。

| 項目 | 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バーガーエージェントへの接続を確認しました。

```ts
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）を変えるだけでテスト↔本番を切り替えられる

## 参考リンク

- [A2A Protocol](https://a2a-protocol.org)
- [What's New in A2A Protocol v1.0](https://github.com/a2aproject/A2A/blob/main/docs/whats-new-v1.md)
- [Google Codelab: A2A Purchasing Concierge](https://codelabs.developers.google.com/intro-a2a-purchasing-concierge)
- [A2A Registry](https://www.a2a-registry.org/)
