結論

A2A(Agent2Agent)プロトコルは、AI エージェント同士が「自分に何ができるか」を Agent Card で公開し、仕事をタスクとして頼み合うための公開仕様です。MCP がエージェントと道具(ツール・データ)をつなぐのに対し、A2A は中身を見せないエージェント同士をつなぎます。仕様は 2026年3月に v1.0 になり、メソッド名が message/send から SendMessage に変わるなど、v0.3 までの解説とは通信の中身が違います。

この記事でわかること

  • LLM を使わない 2 つのエージェントを a2a-sdk 1.3.0 で立て、Agent Card の取得から SendMessage・状態遷移・SSE・成果物までの JSON-RPC の原文
  • 仕様 v0.2・v0.3・v1.0 と SDK 0.2.16・0.3.26・1.3.0 で、メソッド名・Agent Card のパス・列挙値の書き方がどう違うか(実物で確認)
  • MCP との違いの表、input-required で人の確認を挟む流れ、push 通知・認証の要点、採用の判断表

2026年11月4日更新|a2a-sdk 1.3.0(仕様 v1.0)と 0.3.26・0.2.16 を Python 3.13 で実行確認

この記事で実際に動かしたもの:127.0.0.1 に立てた計算エージェント・固定応答エージェント・比較用の 0.3 系エージェント(いずれも LLM なし)と、それを呼ぶ側のエージェント(Python)。

A2A は「LLM の賢さ」を決める仕様ではなく、エージェント同士の名刺交換(Agent Card)と仕事の受け渡し(Task)の書式を決める仕様です。そのため、LLM 本体が無くても通信の中身はすべて確かめられます。この記事では、四則演算だけをするエージェントと決まった文を返すだけのエージェントを相手に、やり取りの原文を順に見ていきます。

flowchart TD
    O[呼ぶ側のエージェント] -->|1. agent-card.json を取得| C[計算エージェント]
    O -->|2. SendMessage| C
    C -->|3. Task と状態・SSE| O
    C -.->|MCP| T[(自分のツール・DB)]

よく調べられることの答えを先にまとめます。

知りたいこと 答え 詳しい節
A2A とは エージェント同士が能力を公開し、タスクを頼み合うための公開仕様。Google が 2025年4月に発表し、Linux Foundation に寄贈、2026年8月から Agentic AI Foundation(AAIF)の下で運営 A2A プロトコルとは
a2a mcp 違い MCP はエージェント↔ツール(縦)、A2A はエージェント↔エージェント(横)。併用が前提 A2A と MCP の違い
仕様の最新版 v1.0(1.0.0 は 2026年3月12日、1.0.1 は 5月28日)。A2A-Version ヘッダーには 1.0 を入れる 仕様の版と SDK の版
agent.json と agent-card.json v0.3.0 で /.well-known/agent-card.json に改名。SDK 0.3 系は両方を返し、1.3.0 は agent.json に 404 仕様の版と SDK の版
message/send が動かない v1.0 では SendMessage。旧名は -32601 Method not found(互換モードを有効にすれば受け付ける) よくあるエラー
タスクの状態 submitted・working・input-required・auth-required・completed・failed・canceled・rejected。v1.0 の JSON では TASK_STATE_COMPLETED の形 タスクの状態遷移
途中経過を受け取る方法 SendStreamingMessage の SSE、GetTask のポーリング、push 通知(Webhook)の 3 つ SSE の原文と到着時刻
人の確認を挟みたい エージェントが input-required で止まり、呼ぶ側が同じ taskId で返事を送る input-required
導入すべきか 相手のエージェントを別チーム・別社が作る、長い処理を待つ、なら候補。社内の 1 プロセスで完結するなら関数呼び出しで済む 採用の判断表

確認環境:Ubuntu 24.04.5 LTS(CPU 2 コア・メモリ 8GB)の Python 3.13.18 で、作業用の venv に PyPI の a2a-sdk 1.3.0(2026年10月27日公開、仕様 v1.0 対応。a2a-sdk[http-server] で Starlette 1.10.0・sse-starlette 3.6.0・httpx 0.28.1・protobuf 7.36.2 が入る)と uvicorn 0.54.0 を入れました。LLM を使わない「計算エージェント」「受付エージェント(固定応答)」を 127.0.0.1 の空きポートで起動し、curl 8.5.0 と httpx・SDK のクライアントから JSON-RPC と HTTP+JSON(REST)で呼んでいます。版の違いは、別の venv の a2a-sdk 0.3.26(仕様 0.3.0)で同じエージェントを立てて確かめ、0.2.16 は配布物(wheel)の中身を読みました。外部のサービスには接続していません。push 通知・認証・gRPC は公式仕様(a2a-protocol.org の Specification・Streaming & Asynchronous Operations・Enterprise-Ready Features・What's New in v1.0)と GitHub のリリース履歴で確かめた範囲で、この環境では動かしていません。図は mermaid 11.16.0 で描画を確かめています(確認日 2026年11月4日)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「A2Aプロトコルとは|MCPとの違いとv1.0の通信を実物で確認」

A2A プロトコルとは:エージェント同士が仕事を頼み合うための約束ごと

この節では、A2A が決めていることと決めていないこと、登場する 4 つの部品、成り立ちと運営の経緯が分かります。

A2A は、作った会社も中の作りも違うエージェント同士が、HTTP の上で「何ができるか」を知らせ合い、仕事を頼み、結果を受け取るための書式と手順を決めた公開仕様です。相手の内部(どの LLM を使うか、どんなツールを持つか、プロンプト)を見せずに協力できることを重視していて、公式ドキュメントは相手を「opaque(中が見えない)」エージェントと呼んでいます。

決めているのは通信の形だけです。エージェントの賢さ、計画の立て方、使う LLM は決めていません。AI エージェントそのものの仕組みは関連記事「AIエージェントとは|仕組みとワークフローとの違い・作り方入門」で扱っています。

1Agent Card(名刺)

名前・説明・スキル・接続先 URL・対応する通信方式・認証方式を書いた JSON。/.well-known/agent-card.json に置くのが基本の見つけ方。

2Message(やり取りの 1 回分)

role(ROLE_USER/ROLE_AGENT)と Part の並び。短い返事ならエージェントは Task を作らず Message だけを返してよい。

3Task(仕事の単位)

id・contextId・状態(status)・履歴・成果物を持つ。長い処理や人の確認を挟む処理はタスクとして追跡する。

4Artifact(成果物)

タスクが出した結果。Message とは別に、名前つきで複数持てる。中身は Part(text・data・url・raw)。

成り立ちは次のとおりです。Google Cloud のブログで 2025年4月に発表され(発表時のパートナーは 50 社以上)、のちに Linux Foundation に寄贈されました。仕様は 0.2 系・0.3.0(2025年7月30日)を経て 1.0.0(2026年3月12日)・1.0.1(2026年5月28日)が公開されています。2026年8月17日には、MCP・AGENTS.md・goose などと同じ Agentic AI Foundation(AAIF)のホストプロジェクトになったと AAIF が発表しました。技術運営委員会には AWS・Cisco・Google・IBM Research・Microsoft・Salesforce・SAP・ServiceNow の代表が入っていると公式サイトに書かれています(2026年11月4日時点)。

Google の発表の設計原則は「エージェントの能力を活用」「既存標準を基盤に構築」「デフォルトでセキュリティを確保」「長時間タスクへの対応」「モダリティに依存しない設計」の 5 つで、中身は HTTP・JSON-RPC 2.0・SSE・OAuth など既にある標準の組み合わせです。

A2A と MCP の違い(エージェント↔ツール と エージェント↔エージェント)

この節では、よく比べられる MCP との役割の違いと、同じシステムの中での併用のしかたが分かります。

MCP はエージェントが自分の道具(ツール・データ・プロンプト)を使うための仕様、A2A はエージェントが別のエージェントに仕事を頼むための仕様です。A2A の公式ドキュメントは「A2A はエージェント同士をつなぎ、MCP は各エージェントを自分のツールにつなぐ」とまとめ、MCP を縦(1 つのエージェントの能力を足す)、A2A を横(チームや組織をまたいでつなぐ)と説明しています。MCP 自体の仕組みは関連記事「MCPとは?サーバーとクライアントの仕組みと設定例を図解」で扱っています。

観点 MCP A2A
つなぐ相手 エージェント(ホスト)↔ ツール・データ(MCP サーバー) エージェント ↔ エージェント
相手の性質 入出力が決まった関数・資源。呼ぶ側の LLM が使い方を決める 自分で考えて動く相手。中身は見えず、結果だけ受け取る
見つけ方 ホストの設定ファイルで登録し、初期化時に tools/list などで一覧を得る Agent Card(/.well-known/agent-card.json・カタログ・直接設定)
仕事の単位 1 回のツール呼び出し(長い処理は拡張の Tasks) Task(状態・履歴・成果物を持ち、複数回のやり取りで進む)
通信 JSON-RPC 2.0。stdio か Streamable HTTP JSON-RPC 2.0・gRPC・HTTP+JSON(REST)の 3 つのバインディング。逐次は SSE
途中で人に聞く Elicitation(サーバーから利用者へ追加情報を求める) input-required・auth-required の状態で止まり、続きの Message を待つ
仕様の版(2026年11月4日時点) 2026-07-28(日付で版を表す) 1.0(Major.Minor で表す)

両者は 1 つのシステムで重ねて使います。図でいえば、呼ぶ側は A2A で計算エージェントに頼み、計算エージェントが自分の DB を使うところは MCP で書けます。

仕様の版と SDK の版:v0.2・v0.3・v1.0 で変わったメソッド名とパス

この節では、ネット上の解説で名前がばらばらに見える理由と、仕様と Python SDK の版の対応、メソッド名・パス・列挙値の新旧対応が分かります。

A2A の解説に出てくるメソッド名やパスが記事ごとに違うのは、仕様が 0.1 → 0.2 → 0.3 → 1.0 と変わるたびに名前が変わったためです。手元で 3 つの版の SDK を確かめた結果と、公式の「What's New in v1.0」・GitHub のリリース履歴を合わせると、次のようになります。

項目 SDK 0.2.16(仕様 0.2 系) SDK 0.3.26(仕様 0.3.0) SDK 1.3.0(仕様 1.0)
Agent Card のパス /.well-known/agent.json agent-card.json(agent.json も返し、警告をログに出す) agent-card.json のみ(agent.json は 404)
送信 message/send message/send SendMessage
逐次の送信 message/stream message/stream SendStreamingMessage
取得・取り消し・再購読 tasks/get・tasks/cancel・tasks/resubscribe 同左 GetTask・CancelTask・SubscribeToTask(ListTasks は 1.0 で追加)
状態・役割の値 "completed"・"user" "completed"・"input-required"・"user" "TASK_STATE_COMPLETED"・"ROLE_USER"
型の見分け方 "kind": "task" など "kind": "status-update" など kind を廃止。task・statusUpdate などのキー名で見分ける
サーバーの書き方 A2AStarletteApplication A2AStarletteApplication create_agent_card_routes・create_jsonrpc_routes などのルート関数

Agent Card のパスの変更は、GitHub のリリース履歴で v0.3.0 の破壊的変更として「Change Well-Known URI for Agent Card hosting from agent.json to agent-card.json」と書かれています。0.3.26 の SDK は移行のために以前のパスでもカードを返し、そのたびに次の警告を出しました。

$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18249/.well-known/agent.json
200
(サーバーのログ)
Deprecated agent card endpoint '/.well-known/agent.json' accessed. Please use '/.well-known/agent-card.json' instead. This endpoint will be removed in a future version.

$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18246/.well-known/agent.json   # 1.3.0
404

v1.0 で変わったメソッド名は、公式の What's New in v1.0 に一覧があります。push 通知の 4 つは tasks/pushNotificationConfig/{set,get,list,delete} が CreateTaskPushNotificationConfig・GetTaskPushNotificationConfig・ListTaskPushNotificationConfigs・DeleteTaskPushNotificationConfig に、拡張カードの取得は agent/getAuthenticatedExtendedCard が GetExtendedAgentCard になりました。v1.0 の名前は gRPC のサービス定義(protobuf)の RPC 名と同じで、1.3.0 の SDK のサーバーも protobuf から作った型で動いています。

ほかに v1.0 で変わった主な点は、Agent Card の最上位の url・protocolVersion・preferredTransport が無くなり supportedInterfaces[] に移ったこと、REST のパスから /v1 が外れたこと、エラーの data が google.rpc の ErrorInfo の配列になったこと、ページ送りがカーソル方式になったこと、逐次の最後を示す final が無くなりストリームが閉じることで終わりを示すようになったこと、です。

なお、解説記事には tasks/send・tasks/sendSubscribe という名前も見られます。0.2.16・0.3.26・1.3.0 のどの SDK の中を探してもこの名前は無く、0.2 より前の版の名前です。今から作る場合はどちらも使いません。

解説記事のコードを写すとき

やってはいけない記事の日付と版を見ずに message/send・"role": "user"・/.well-known/agent.json を写す。1.3.0 のサーバーには -32601・-32602・404 で返される
→
代わりに相手の Agent Card の supportedInterfaces[].protocolVersion(v0.3 なら最上位の protocolVersion)を先に読み、版に合う名前で書く。SDK のクライアントを使えば版の選択は自動

判断:新しく作るなら v1.0。相手に 0.3 のエージェントが残るなら、サーバー側で互換モードを有効にするか、1.x の SDK のクライアントに任せる(後の節で実測)

最初の 5 分:LLM なしの 2 つのエージェントを 127.0.0.1 で動かす

この節では、a2a-sdk 1.3.0 で最小のエージェントを立てる手順と、Agent Card の実物が分かります。

エージェント側で書くのは AgentExecutor の execute() だけで、受け取った Message を読み、イベント(Task・状態の更新・成果物)をキューに入れていきます。JSON-RPC の受け口・タスクの保存・SSE の送出は SDK が受け持ちます。

1

venv に SDK を入れる

pip install "a2a-sdk[http-server]==1.3.0" uvicorn。http-server の extra で Starlette と sse-starlette が入る。キャッシュを含めて 65MB でした。

Python 3.10 以上

2

AgentExecutor を書く

計算エージェントは Task を作って working → 成果物 → completed。受付エージェントは Message を 1 つ返すだけ。

LLM は呼ばない

3

Agent Card を作る

スキル・supportedInterfaces(JSON-RPC と REST の URL)・capabilities.streaming を書く。

v1.0 では url を最上位に書かない

4

ルートを組み立てて起動

カード・JSON-RPC・REST のルートを Starlette に渡し、uvicorn で 127.0.0.1 の空きポートに立てる。

計算 18246・受付 18247

計算エージェントの中心部分です(式の評価は ast で四則演算だけを許す自作の関数 safe_eval。全体は約 120 行)。

from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.server.tasks import TaskUpdater
from a2a.helpers import new_task_from_user_message, new_data_part
from a2a.types import Part, TaskState

PENDING = {}  # input-required で止めたタスクの式

class CalcExecutor(AgentExecutor):
    async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
        text = context.get_user_input().strip()
        task = context.current_task
        if task is None:
            task = new_task_from_user_message(context.message)
            await event_queue.enqueue_event(task)            # submitted
        up = TaskUpdater(event_queue, task.id, task.context_id)
        await up.start_work()                                 # working
        await asyncio.sleep(0.3)                              # 重い処理の代わり
        if task.id in PENDING:                                # 確認への返事
            if text not in ("はい", "yes"):
                await up.cancel(up.new_agent_message([Part(text="取り消しました")]))
                return
            text = PENDING.pop(task.id)
        elif len(text) > 20:                                  # 長い式は人の確認を挟む
            await up.update_status(TaskState.TASK_STATE_INPUT_REQUIRED,
                message=up.new_agent_message([Part(text=f"式が長いので確認します。「{text}」を計算してよいですか(はい/いいえ)")]))
            PENDING[task.id] = text
            return
        try:
            v = safe_eval(text)
        except Exception as e:
            await up.failed(up.new_agent_message([Part(text=f"計算できません: {e}")]))
            return
        await up.add_artifact([Part(text=f"{text} = {v}")], name="result")
        await up.add_artifact([new_data_part({"expression": text, "value": v})], name="result.json")
        await up.complete()

Agent Card とルートの組み立てです。

card = AgentCard(
    name="calc-agent", description="四則演算だけをするエージェント(LLM なし)", version="0.1.0",
    supported_interfaces=[
        AgentInterface(url=f"{base}/a2a/jsonrpc", protocol_binding="JSONRPC", protocol_version="1.0"),
        AgentInterface(url=f"{base}/a2a/rest", protocol_binding="HTTP+JSON", protocol_version="1.0"),
    ],
    capabilities=AgentCapabilities(streaming=True, push_notifications=False),
    default_input_modes=["text/plain"], default_output_modes=["text/plain", "application/json"],
    skills=[AgentSkill(id="arith", name="四則演算", description="+ - * / だけの式を計算します",
                       tags=["math"], examples=["12*7+3"])])
handler = DefaultRequestHandler(agent_executor=CalcExecutor(), task_store=InMemoryTaskStore(), agent_card=card)
routes = (create_agent_card_routes(card)
          + create_jsonrpc_routes(handler, "/a2a/jsonrpc")
          + create_rest_routes(handler, path_prefix="/a2a/rest"))
uvicorn.run(Starlette(routes=routes), host="127.0.0.1", port=18246)

起動したら、まず Agent Card を取ります。ここが A2A の入口で、呼ぶ側はこのカードだけを見て「誰に・どの URL で・どの方式で」頼むかを決めます。

$ curl -s -i http://127.0.0.1:18246/.well-known/agent-card.json
HTTP/1.1 200 OK
server: uvicorn
etag: W/"ff71ad7458cb205da8ec42ebfc95738f5c4781f4478ff758ac378b7be93f62c4"
content-length: 625
content-type: application/json

{"name":"calc-agent","description":"四則演算だけをするエージェント(LLM なし)",
 "supportedInterfaces":[
   {"url":"http://127.0.0.1:18246/a2a/jsonrpc","protocolBinding":"JSONRPC","protocolVersion":"1.0"},
   {"url":"http://127.0.0.1:18246/a2a/rest","protocolBinding":"HTTP+JSON","protocolVersion":"1.0"}],
 "version":"0.1.0",
 "capabilities":{"streaming":true,"pushNotifications":false},
 "defaultInputModes":["text/plain"],"defaultOutputModes":["text/plain","application/json"],
 "skills":[{"id":"arith","name":"四則演算","description":"+ - * / だけの式を計算します",
            "tags":["math"],"examples":["12*7+3"]}]}
(見やすさのため改行を入れています)

$ curl -s -o /dev/null -w '%{http_code}\n' -H 'If-None-Match: W/"ff71ad74…"' http://127.0.0.1:18246/.well-known/agent-card.json
304

1.3.0 の SDK はカードに弱い ETag を付け、If-None-Match が合えば 304 を返しました。カードを何度も取りに来る呼ぶ側の負担を減らす仕組みです。JSON のキーは camelCase(Python 側の supported_interfaces が supportedInterfaces になる)で、値の入っていない項目(provider・securitySchemes など)は出力されません。

自分で確かめる(1):相手のエージェントが A2A に対応しているかは、curl -s https://相手のホスト/.well-known/agent-card.json の 1 行で分かります。返ってきた JSON に supportedInterfaces があれば v1.0、最上位に protocolVersion("0.3.0" など)と url があれば v0.3 のカードです。

SendMessage の JSON-RPC 原文:Message で即答する場合と Task を返す場合

この節では、依頼の送り方と、返事が Message になる場合と Task になる場合の違い、REST で同じことをしたときの形が分かります。

依頼は JSON-RPC 2.0 の SendMessage で、本文の params.message に messageId・role・parts を入れ、HTTP ヘッダー A2A-Version: 1.0 を付けて POST します。受付エージェント(固定応答)は Task を作らずに Message だけを返しました。

$ curl -s -H 'Content-Type: application/json' -H 'A2A-Version: 1.0' \
  -d '{"jsonrpc":"2.0","id":7,"method":"SendMessage","params":{"message":{
        "messageId":"m-7","role":"ROLE_USER","parts":[{"text":"こんにちは"}]}}}' \
  http://127.0.0.1:18247/a2a/jsonrpc

{"result":{"message":{"messageId":"286870eb-…","contextId":"fb17f3c6-…","role":"ROLE_AGENT",
  "parts":[{"text":"受付エージェントです。計算は calc エージェントに依頼してください。"}]}},
 "id":7,"jsonrpc":"2.0"}

計算エージェントに式を送ると、result の中が message ではなく task になり、状態・成果物・履歴が付いてきます。SendMessage は既定で、タスクが終端か中断の状態になるまで待ってから返します(0.3 の blocking にあたる設定で、v1.0 では configuration.returnImmediately が逆の意味の設定)。

$ curl -s -H 'Content-Type: application/json' -H 'A2A-Version: 1.0' \
  -d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":{
        "messageId":"m-1","role":"ROLE_USER","parts":[{"text":"12*7+3"}]}}}' \
  http://127.0.0.1:18246/a2a/jsonrpc

{"result":{"task":{
   "id":"2fb7f4b4-1bbd-44f0-9b68-8e2ed8ca4517",
   "contextId":"af1ef627-ee3c-42ac-968f-e27265ed7c64",
   "status":{"state":"TASK_STATE_COMPLETED","timestamp":"2026-11-03T19:14:42.524280Z"},
   "artifacts":[
     {"artifactId":"18a40943-…","name":"result","parts":[{"text":"12*7+3 = 87"}]},
     {"artifactId":"68a1902e-…","name":"result.json",
      "parts":[{"data":{"value":87.0,"expression":"12*7+3"}}]}],
   "history":[{"messageId":"m-1","contextId":"af1ef627-…","taskId":"2fb7f4b4-…",
               "role":"ROLE_USER","parts":[{"text":"12*7+3"}]}]}},
 "id":1,"jsonrpc":"2.0"}
(UUID の後半は … で省略。timestamp は UTC で、日本時間では 11月4日 4時14分)

Message で返すか Task で返すかはエージェントが決め、呼ぶ側は result の下のキー名で分けて扱います。

configuration で受け取り方を調整できます。"historyLength":0 にすると履歴が省かれ、応答が小さくなります。"returnImmediately":true にすると、処理の終わりを待たずに submitted の状態ですぐ返りました。

{"jsonrpc":"2.0","id":21,"method":"SendMessage","params":{"message":{…"parts":[{"text":"2*3"}]},
 "configuration":{"returnImmediately":true,"historyLength":0}}}

{"result":{"task":{"id":"3cb519bd-…","contextId":"266dd7b2-…","status":{"state":"TASK_STATE_SUBMITTED"}}},
 "id":21,"jsonrpc":"2.0"}

同じ依頼を HTTP+JSON(REST)バインディングで送ると、JSON-RPC の封筒(jsonrpc・id・method)が無くなり、URL がメソッドを表します。応答の Content-Type は application/a2a+json でした。

$ curl -s -i -H 'Content-Type: application/json' -H 'A2A-Version: 1.0' \
  -d '{"message":{"messageId":"r-1","role":"ROLE_USER","parts":[{"text":"6*7"}]},
       "configuration":{"historyLength":0}}' \
  http://127.0.0.1:18246/a2a/rest/message:send
HTTP/1.1 200 OK
content-type: application/a2a+json

{"task":{"id":"94005a86-…","contextId":"e675e56a-…",
  "status":{"state":"TASK_STATE_COMPLETED","timestamp":"2026-11-03T19:15:46.006717Z"},
  "artifacts":[{"artifactId":"b1b7a445-…","name":"result","parts":[{"text":"6*7 = 42"}]}, …]}}
操作 JSON-RPC の method REST(1.3.0 の SDK のルート)
送信SendMessagePOST /message:send
逐次の送信SendStreamingMessagePOST /message:stream
タスク取得GetTaskGET /tasks/{id}
一覧ListTasksGET /tasks
取り消しCancelTaskPOST /tasks/{id}:cancel
再購読SubscribeToTaskGET または POST /tasks/{id}:subscribe
push 通知の設定Create・Get・List・Delete の 4 つ/tasks/{id}/pushNotificationConfigs と /{push_id}
拡張カードGetExtendedAgentCardGET /extendedAgentCard

固定応答の受付エージェントに同じマシンから 300 回ずつ送った所要時間です。プロトコルの手間は数ミリ秒で、実際の所要はエージェントの処理(LLM の推論など)で決まります。

1.19msAgent Card の GET(中央値)p95 8.58ms
4.15msJSON-RPC の SendMessage(中央値)p95 8.03ms・Message で即答
4.35msREST の message:send(中央値)p95 9.07ms・JSON-RPC との差は誤差の範囲

タスクの状態遷移(TASK_STATE_*):終端・中断・進行中の 3 種類

この節では、タスクの 8 つの状態と、どれが「もう続かない状態」でどれが「返事を待つ状態」か、実際にどう遷移したかが分かります。

タスクの状態は、進行中(submitted・working)、中断(input-required・auth-required)、終端(completed・failed・canceled・rejected)の 3 種類に分かれます。終端に入ったタスクには、もう Message を送れません。中断は「相手の返事待ち」で、同じ taskId に続きの Message を送ると再開します。

flowchart TD
    S[submitted] --> W[working]
    W --> D{どうなったか}
    D -->|終端| E[completed / failed<br>canceled / rejected]
    D -->|中断| I[input-required<br>auth-required]
    I -->|同じ taskId で返事| W
状態(v0.3 の値) v1.0 の JSON の値 種類 この環境で起こした方法
submittedTASK_STATE_SUBMITTED進行中returnImmediately・SSE の最初のイベント
workingTASK_STATE_WORKING進行中start_work()
input-requiredTASK_STATE_INPUT_REQUIRED中断20 文字を超える式
auth-requiredTASK_STATE_AUTH_REQUIRED中断(公式仕様の範囲)
completedTASK_STATE_COMPLETED終端計算できた
failedTASK_STATE_FAILED終端1/0(division by zero)
canceledTASK_STATE_CANCELED終端確認に「いいえ」・CancelTask
rejectedTASK_STATE_REJECTED終端(エージェントが受けないと決めたとき)

failed になった例です。計算できない理由は、タスクの status.message にエージェントの Message として入ります。

{"result":{"task":{"id":"119048fa-…","status":{"state":"TASK_STATE_FAILED",
  "message":{"messageId":"903e9647-…","role":"ROLE_AGENT",
             "parts":[{"text":"計算できません: division by zero"}]},
  "timestamp":"2026-11-03T19:14:42.926035Z"}, …}},"id":6,"jsonrpc":"2.0"}

終端に入ったタスクへの操作は、どれもエラーになりました。

(failed のタスクに続きの Message を送る)
{"error":{"code":-32004,"message":"Task 7389948f-… is in terminal state: TASK_STATE_FAILED",
  "data":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"UNSUPPORTED_OPERATION",
           "domain":"a2a-protocol.org","metadata":{}}]},"id":11,"jsonrpc":"2.0"}

(同じタスクを CancelTask)
{"error":{"code":-32002,"message":"Task cannot be canceled",
  "data":[{…"reason":"TASK_NOT_CANCELABLE",…}]},"id":12,"jsonrpc":"2.0"}

(canceled のタスクを SubscribeToTask)
{"error":{"code":-32004,"message":"Task 1311d977-… is in terminal state: TASK_STATE_CANCELED", …}}

処理中(working)のタスクは CancelTask で止められます。returnImmediately で依頼した直後に CancelTask を送ると、0.3 秒の処理の途中でも結果は TASK_STATE_CANCELED になり、その後の GetTask でも canceled のままでした。SDK は execute() を動かしている asyncio のタスクを取り消し、エージェントの cancel() を呼びます。

input-required で人の確認を挟む流れ

この節では、エージェントが途中で確認を求め、呼ぶ側が返事を送って処理が再開するまでの原文と、設計の注意点が分かります。

エージェントは input-required の状態と質問の Message を返して execute() を抜け、呼ぶ側が同じ taskId(と contextId)を付けた Message を送ると、SDK が同じタスクで execute() をもう一度呼びます。計算エージェントでは、20 文字を超える式を「確認が要る依頼」に見立てました。

(1 回目:長い式を送る)
{"jsonrpc":"2.0","id":8,"method":"SendMessage","params":{"message":{
  "messageId":"m-8","role":"ROLE_USER","parts":[{"text":"(1200+340)*12-560/7+99"}]}}}

{"result":{"task":{"id":"88c53d7f-…","contextId":"c2546d81-…",
  "status":{"state":"TASK_STATE_INPUT_REQUIRED",
    "message":{"role":"ROLE_AGENT","parts":[{"text":"式が長いので確認します。「(1200+340)*12-560/7+99」を計算してよいですか(はい/いいえ)"}]},
    …}}},"id":8,"jsonrpc":"2.0"}

(2 回目:同じ taskId と contextId で返事を送る)
{"jsonrpc":"2.0","id":10,"method":"SendMessage","params":{"message":{
  "messageId":"m-9","role":"ROLE_USER","taskId":"88c53d7f-…","contextId":"c2546d81-…",
  "parts":[{"text":"はい"}]},"configuration":{"historyLength":0}}}

{"result":{"task":{"id":"88c53d7f-…","status":{"state":"TASK_STATE_COMPLETED",…},
  "artifacts":[{"name":"result","parts":[{"text":"(1200+340)*12-560/7+99 = 18499.0"}]},
               {"name":"result.json","parts":[{"data":{"value":18499.0,"expression":"(1200+340)*12-560/7+99"}}]}]}},
 "id":10,"jsonrpc":"2.0"}

「いいえ」と返すと、エージェントは canceled にして「取り消しました」を返しました。止まっている間に GetTask で見ると、状態は input-required のまま、質問の Message も残っています。呼ぶ側が一度切れても、taskId さえ控えておけば後から状態を確かめて返事を送れます。

はまった点を 1 つ書いておきます。最初の版では「再開かどうか」を context.current_task.status.state == TASK_STATE_INPUT_REQUIRED で判定していましたが、その前に start_work() を呼んでいたため、判定の時点で状態はもう working になっていて、「はい」が式として評価され failed になりました。再開の判定は、状態を変える前に読むか、この例のようにエージェント自身の記録(PENDING)で行います。

確認待ちの内容をどこに持つか

やってはいけないこの例のようにプロセスのメモリ(辞書)だけに持つ。再起動や複数台構成で、返事が来ても続きが分からなくなる
→
代わりにTaskStore を DB にする(1.3.0 には SQL 用の DatabaseTaskStore があり、extra の sql・postgresql・mysql・sqlite で入る)か、確認待ちの内容を自分の DB に taskId で保存する

判断:試作は InMemoryTaskStore、人の返事を何分も待つ本番は永続化が前提。待つ時間の上限と、時間切れのときに canceled か failed にする処理も決めておく

人が承認するまで止める仕組みは、エージェントの設計パターンでいう「人の確認(Human-in-the-loop)」にあたります。どこに確認を挟むかの考え方は関連記事「AIエージェントの設計パターン一覧|5つの型の違いと選び方」で扱っています。

SendStreamingMessage の SSE 原文と到着時刻

この節では、逐次の送信で何が・どの順で・どの間隔で届くか、ストリームがいつ閉じるかが分かります。

SendStreamingMessage を送ると、HTTP 200・Content-Type: text/event-stream の応答の中に、JSON-RPC の応答が data: 行として 1 つずつ届きます。各行の result には task・statusUpdate・artifactUpdate・message のどれか 1 つが入ります。

$ curl -s -N -i -H 'Content-Type: application/json' -H 'A2A-Version: 1.0' \
  -d '{"jsonrpc":"2.0","id":20,"method":"SendStreamingMessage","params":{"message":{
        "messageId":"m-20","role":"ROLE_USER","parts":[{"text":"12*7+3"}]},
        "configuration":{"historyLength":0}}}' http://127.0.0.1:18246/a2a/jsonrpc
HTTP/1.1 200 OK
cache-control: no-store
connection: keep-alive
x-accel-buffering: no
content-type: text/event-stream; charset=utf-8
Transfer-Encoding: chunked

data: {"result": {"task": {"id": "e622b306-…", "contextId": "3ae4ff82-…", "status": {"state": "TASK_STATE_SUBMITTED"}}}, "id": 20, "jsonrpc": "2.0"}

data: {"result": {"statusUpdate": {"taskId": "e622b306-…", "contextId": "3ae4ff82-…", "status": {"state": "TASK_STATE_WORKING", "timestamp": "2026-11-03T19:15:21.962011Z"}}}, "id": 20, "jsonrpc": "2.0"}

data: {"result": {"artifactUpdate": {"taskId": "e622b306-…", "contextId": "3ae4ff82-…", "artifact": {"artifactId": "acccb4d8-…", "name": "result", "parts": [{"text": "12*7+3 = 87"}]}}}, "id": 20, "jsonrpc": "2.0"}

data: {"result": {"artifactUpdate": {… "name": "result.json", "parts": [{"data": {"value": 87.0, "expression": "12*7+3"}}]}}}, "id": 20, "jsonrpc": "2.0"}

data: {"result": {"statusUpdate": {"taskId": "e622b306-…", "contextId": "3ae4ff82-…", "status": {"state": "TASK_STATE_COMPLETED", "timestamp": "2026-11-03T19:15:22.262713Z"}}}, "id": 20, "jsonrpc": "2.0"}
(ここでストリームが閉じる)

x-accel-buffering: no は、間に nginx を置いたときに SSE がまとめて溜められないようにするヘッダーで、SDK が自動で付けていました。どのイベントにも同じ JSON-RPC の id(20)が付きます。

到着時刻を httpx で測ると、エージェントの処理(0.3 秒の待ち)がそのまま間隔に表れました。2 回目は確認が要る長い式で、input-required が届いたところでストリームが閉じています。公式の Streaming & Asynchronous Operations にも、終端か中断の状態(COMPLETED・FAILED・CANCELED・REJECTED・INPUT_REQUIRED)でサーバーはストリームを閉じると書かれています。

$ python stream_timing.py '12*7+3'
  195.2 ms  task            TASK_STATE_SUBMITTED
  195.2 ms  statusUpdate    TASK_STATE_WORKING
  492.7 ms  artifactUpdate  result
  492.7 ms  artifactUpdate  result.json
  492.7 ms  statusUpdate    TASK_STATE_COMPLETED

$ python stream_timing.py '(1200+340)*12-560/7+99'
   83.6 ms  task            TASK_STATE_SUBMITTED
   83.7 ms  statusUpdate    TASK_STATE_WORKING
  380.1 ms  statusUpdate    TASK_STATE_INPUT_REQUIRED
(ここで閉じる。返事は新しい SendStreamingMessage で送る)

0.3.26 の SDK で同じ処理を動かした原文と比べます。

data: {"id":3,"jsonrpc":"2.0","result":{"contextId":"23c6f23c-…","final":false,"kind":"status-update",
       "status":{"state":"working",…},"taskId":"6dd6a0ea-…"}}
data: {"id":3,"jsonrpc":"2.0","result":{"artifact":{…"parts":[{"kind":"text","text":"= 42"}]},…"kind":"artifact-update",…}}
data: {"id":3,"jsonrpc":"2.0","result":{…"final":true,"kind":"status-update","status":{"state":"completed",…},…}}

0.3 は kind で型を、final: true で最後を示していました。v1.0 はどちらも無くし、型はキー名(statusUpdate など)で、最後はストリームが閉じることで示します。0.3 用に「final を待つ」書き方をしたクライアントは、v1.0 では最後を見つけられません。

途中で接続が切れたときは、タスクが進行中なら SubscribeToTask(0.3 の tasks/resubscribe)で続きを受け取れます。returnImmediately で依頼した直後に SubscribeToTask を送ると、現在のタスク(working)から始まり、成果物 2 つと completed が届いて閉じました。

成果物(Artifact)と Part:text・data・url・raw の使い分け

この節では、結果の入れ物である Artifact と、その中身の Part の 4 つの形、数値の型の落とし穴が分かります。

成果物は Artifact として、名前を付けて 1 つのタスクに複数持てます。中身の Part は v1.0 で 1 つの型にまとまり、text・data(任意の JSON)・url(ファイルの場所)・raw(バイト列)のどれか 1 つを持ちます。計算エージェントでは、人が読む文(text)と、プログラムが読む JSON(data)を別の Artifact にしました。

Part の中身 v1.0 のキー v0.3 での書き方 向く用途
文字text{"kind":"text","text":…}人に見せる返事・要約
構造化データdata{"kind":"data","data":{…}}呼ぶ側のプログラムが読む結果・フォームの値
ファイルの場所urlfile.uri(FilePart)大きなファイル・画像。相手が取りに行く
ファイルの中身raw(JSON では base64)file.bytes(FilePart)小さなファイルを同梱

どの Part にも mediaType(0.3 の mimeType)と filename を付けられます。

落とし穴が 1 つありました。12*7+3 の結果 87 は整数ですが、data の中では 87.0 になって返ってきます。1.3.0 の SDK の new_data_part の説明にも「google.protobuf.Value は数値をすべて double で持つので、整数は float として読み戻される(4326 は 4326.0 になる)」と書かれています。ID や金額のように桁が大きい整数・小数の誤差が困る値は、data に数値で入れず文字列で入れるのが安全です。

長い成果物は、同じ artifactId で append: true を付けて少しずつ送り、最後に lastChunk: true を付けます。

呼ぶ側のエージェント:Agent Card のスキルで相手を選び、版を自動で合わせる

この節では、エージェントが別のエージェントを見つけて依頼する一連の流れを SDK のクライアントで動かした結果と、0.3 のエージェントとつないだときの動きが分かります。

呼ぶ側のエージェントは、候補の Agent Card を取り、スキルのタグで相手を選び、カードに書かれた接続先へ依頼します。判断に LLM を使う代わりに「式なら math のタグ、それ以外は fixed のタグ」という規則で選ぶ、50 行ほどの呼ぶ側を書きました。httpx のフックで実際に出たリクエストも表示しています。

cards = [await A2ACardResolver(http, u).get_agent_card() for u in AGENTS]
want = "math" if re.fullmatch(r"[\d+\-*/(). ]+", text) else "fixed"
card = next(c for c in cards if any(want in s.tags for s in c.skills))
client = await create_client(card, ClientConfig(httpx_client=http, streaming=True))
msg = Message(message_id=str(uuid.uuid4()), role=Role.ROLE_USER, parts=[Part(text=text)])
async for ev in client.send_message(SendMessageRequest(message=msg)):
    kind = ev.WhichOneof("payload")   # task / message / status_update / artifact_update
    ...
=== 12*7+3
  >> GET /.well-known/agent-card.json
  >> GET /.well-known/agent-card.json
選んだ相手: calc-agent(skill tag=math)
  >> POST /a2a/jsonrpc  A2A-Version=1.0  method=SendStreamingMessage
  << task TASK_STATE_SUBMITTED
  << status_update TASK_STATE_WORKING
  << artifact_update result ['12*7+3 = 87']
  << artifact_update result.json [{'value': 87.0, 'expression': '12*7+3'}]
  << status_update TASK_STATE_COMPLETED

=== (1200+340)*12-560/7+99
選んだ相手: calc-agent(skill tag=math)
  >> POST /a2a/jsonrpc  A2A-Version=1.0  method=SendStreamingMessage
  << task TASK_STATE_SUBMITTED
  << status_update TASK_STATE_WORKING
  << status_update TASK_STATE_INPUT_REQUIRED
  ** 相手からの確認: 式が長いので確認します。「(1200+340)*12-560/7+99」を計算してよいですか(はい/いいえ) → 人が「はい」と答えた想定
  >> POST /a2a/jsonrpc  A2A-Version=1.0  method=SendStreamingMessage
  << status_update TASK_STATE_WORKING
  << artifact_update result ['(1200+340)*12-560/7+99 = 18499.0']
  << artifact_update result.json [{'value': 18499.0, 'expression': '(1200+340)*12-560/7+99'}]
  << status_update TASK_STATE_COMPLETED

=== こんにちは
選んだ相手: fixed-agent(skill tag=fixed)
  >> POST /a2a/jsonrpc  A2A-Version=1.0  method=SendStreamingMessage
  << message 受付エージェントです。計算は calc エージェントに依頼してください。

SDK のクライアントは、カードの capabilities.streaming を見て SendStreamingMessage を選び、A2A-Version: 1.0 を自動で付けました。「人が答えた想定」の部分は、実際のシステムでは画面での承認やチャットでの返事になります。

次に、同じ 1.3.0 のクライアントを 0.3.26 の SDK で立てたエージェント(カードの protocolVersion は "0.3.0")に向けました。

  >> GET /.well-known/agent-card.json
  >> / A2A-Version= 0.3 b'{"method":"message/send","params":{"configuration":{"blockin'
  << task TASK_STATE_COMPLETED = 87

クライアントはカードの版を読み、0.3 の名前(message/send)と 0.3 の書式で送り、受け取った結果を v1.0 の型に変換して返しました。呼ぶ側を 1.x の SDK にしておけば、相手が 0.3 のままでも書き分けずに済みます。逆向き(0.3 の書式で 1.3.0 のサーバーを呼ぶ)は、サーバー側で create_jsonrpc_routes(..., enable_v0_3_compat=True) を有効にすると、同じ URL で message/send を受け付け、0.3 の書式("kind":"task"・"state":"completed")で返しました。互換モードを有効にしても、A2A-Version: 1.0 を付けた SendMessage はそのまま v1.0 として処理されます。

push 通知と認証(公式仕様の範囲)

この節では、長い処理の結果を Webhook で受け取る push 通知の仕組みと、A2A での認証・認可の考え方が分かります。どちらもこの環境では動かしておらず、公式仕様と SDK のエラー応答で確かめた範囲です。

push 通知は、呼ぶ側が Webhook の URL をタスクに登録し、エージェントが状態の大きな変化(終端・input-required・auth-required など)のたびに、その URL へ StreamResponse の形の JSON を POST する仕組みです。数分〜数時間かかる処理で、SSE の接続を開けっぱなしにしたくないときに使います。登録は SendMessage の configuration に含めるか、既存のタスクに CreateTaskPushNotificationConfig で行います。設定の主な項目は url(HTTPS の Webhook)・token(受け手が照合する値、任意)・authentication(エージェントが Webhook に名乗る方法、任意)で、Agent Card の capabilities.pushNotifications が true のエージェントだけが対応します。

手元の計算エージェントは pushNotifications を false にしているので、登録しようとすると次のエラーになりました。

{"error":{"code":-32003,"message":"Push notifications are not supported by the agent",
  "data":[{…"reason":"PUSH_NOTIFICATION_NOT_SUPPORTED","domain":"a2a-protocol.org",…}]},"id":15,"jsonrpc":"2.0"}

公式の Streaming & Asynchronous Operations は、Webhook について、エージェントは渡された URL へ無条件に POST してはならない(許可ドメインの一覧・所有の確認・外向き通信の制限で防ぐ。1.3.0 の SDK にも URL を検査する push_url_validator の引数がある)、エージェントは Webhook に自分を認証しなければならない(Bearer・API キー・HMAC・mTLS)、受け手は署名やトークンを照合し時刻と一意な ID で再送を拒まなければならない、と定めています。通知を受けた側は GetTask で最新のタスクを取り直します。

認証は、メッセージの本文ではなく HTTP の層で行います。公式の Enterprise-Ready Features は「A2A の本文(JSON-RPC のメッセージなど)は利用者や呼ぶ側の身元を直接は運ばない」とし、資格情報は Authorization: Bearer … などの標準の HTTP ヘッダーで送る、と定めています。エージェントは対応する方式を Agent Card の securitySchemes と securityRequirements(v1.0。0.3 では security)に書きます。v1.0 の型で選べる方式は API キー・HTTP 認証・OAuth 2.0・OpenID Connect・mTLS で、OAuth のフローは v1.0 で implicit と password が無くなり、デバイスコードと PKCE 必須の指定が加わりました。

決めること 公式仕様の要点 設計のときに見るところ
通信路本番は HTTPS が必須。TLS 1.2 以上を推奨し、証明書を検証する社内同士でも平文の HTTP にしない
身元HTTP ヘッダーで送る。足りなければ 401 と WWW-Authenticate、権限が無ければ 403資格情報の入手は A2A の外(OAuth のフローなど)
権限スキルごとに許可を分けられる(OAuth のスコープで一部のスキルだけ許すなど)。最小権限エージェントが裏の業務システムを触る前に、呼ぶ側の権限を確かめる
タスク中の追加の認証auth-required で止め、呼ぶ側が別途入手した資格情報で再開資格情報を Message の本文に入れない運用にする

認証した相手にだけ詳しいカードを見せる GetExtendedAgentCard は、用意していない計算エージェントでは -32004 The agent does not support authenticated extended cards を返しました。エージェントが他の仕組みとどう境界を持つか(権限・ログ・止め方)は、関連記事「ハーネスとは|AIエージェントの仕組みとハーネスエンジニアリング入門」で扱っています。

よくあるエラー・つまずき(原文つき)

この節では、A2A のエージェントを作る・呼ぶときに実際に出たエラーと、原因と直し方が一覧で分かります。

A2A 固有のエラーは JSON-RPC の -32001〜-32009 で、data に google.rpc の ErrorInfo(reason と domain)が付きます。1.3.0 の SDK で割り当てられている番号は、-32001 TaskNotFound・-32002 TaskNotCancelable・-32003 PushNotificationNotSupported・-32004 UnsupportedOperation・-32005 ContentTypeNotSupported・-32006 InvalidAgentResponse・-32007 ExtendedAgentCardNotConfigured・-32008 ExtensionSupportRequired・-32009 VersionNotSupported です。

症状・原文 原因 直し方
-32009 A2A version '0.3' is not supported by this handler. Expected version '1.0'. A2A-Version ヘッダーを付けずに v1.0 の SendMessage を送った。仕様は「空の値は 0.3 と解釈しなければならない」 A2A-Version: 1.0 を付ける(クエリ ?A2A-Version=1.0 でも可)
-32601 Method not found 1.3.0 のサーバーに 0.3 の message/send、または 0.3 のサーバーに SendMessage を送った 相手のカードの版に合わせる。サーバー側なら enable_v0_3_compat=True
-32602 Validation failed・"field":"message.role","message":"Field is required." role を 0.3 の "user" で送った。v1.0 の列挙値として読めず、未指定と同じ扱いになる "role":"ROLE_USER"。値が違うのに「必須」と出るので見落としやすい
-32001 Task not found 存在しない taskId。InMemoryTaskStore はサーバーの再起動で消える TaskStore を DB にする。taskId を控える側も、消えた前提の処理を持つ
-32004 Task … is in terminal state: TASK_STATE_FAILED 終端のタスクに続きを送った・SubscribeToTask した 新しいタスクとして送る(同じ contextId を付ければ会話の続きとして扱える)
REST で 400 FAILED_PRECONDITION(VERSION_NOT_SUPPORTED)・404 NOT_FOUND(TASK_NOT_FOUND) REST バインディングでは同じエラーが HTTP の状態コードと google.rpc.Status の形で返る JSON-RPC と同じ直し方。REST はエラーの Content-Type が application/json(0.3 は problem+json)
/.well-known/agent.json が 404 1.x の SDK は v0.3.0 で改名された agent-card.json だけを返す パスを agent-card.json に直す
data の整数が 87.0 になる protobuf の Value は数値を double で持つ 桁の大きい整数や金額は文字列で入れる
input-required の返事が別のタスクとして処理される 返事の Message に taskId を付けていない statusUpdate に入っていた taskId・contextId を付けて送る

v1.0 では JSON-RPC の data が ErrorInfo の配列になり、reason で機械的に分けられます。0.3 の頃のコードが error.data を辞書として読んでいれば直します。

A2A の採用の判断表:入れる場面・見送る場面

この節では、A2A を入れるかどうかを決める順番と、場面ごとの判断の目安が分かります。

A2A が効くのは「相手のエージェントを自分たちで中まで変えられない」ときです。次の順に考えると判断がぶれません。

  1. 11 つのプロセス・1 つのチームで完結する → A2A は要らないフレームワーク内のサブエージェントや関数呼び出しで済む。A2A にすると状態の管理と HTTP の往復が増えるだけ
  2. 2相手が入出力の決まった道具 → MCP検索・DB・ファイル・社内 API。呼ぶ側の LLM が使い方を決める
  3. 3相手が別チーム・別会社のエージェントで、中を見せない → A2AAgent Card で能力を知り、タスクで頼む。言語やフレームワークを合わせなくてよい
  4. 4処理が長い・途中で人の確認や追加の認証が要る → A2A のタスクinput-required・auth-required・SubscribeToTask・push 通知がそのまま使える
  5. 5使っている製品が A2A のエージェントを公開・接続できる → まず製品の機能でクラウドやエージェント製品側の対応状況は製品ごとに違うので、各社の公式ドキュメントで版(0.3 か 1.0 か)を確かめる
場面 判断 理由
社内の複数部署が作ったエージェントを 1 つの窓口から呼びたいA2A が向く部署ごとに言語・LLM が違っても、カードとタスクの形をそろえれば呼べる
取引先のエージェントに見積もりや在庫確認を頼みたいA2A が向く(認証の設計が先)中を見せない前提の仕様。OAuth のスコープでスキルごとに権限を分けられる
1 つのアプリの中で、計画役と実行役のエージェントを分けたい見送る同じプロセスなら関数呼び出しで十分速い。後で外に出すときに A2A でつなぐ
相手の多くが 0.3 のまま呼ぶ側を 1.x の SDK に、サーバーは互換モードこの記事の実測で、1.3.0 のクライアントは 0.3 のカードを見て書式を切り替えた

MCP サーバーを自作する手順は関連記事「MCPサーバーの作り方|Pythonの完成コードとClaude Code登録」で扱っています。A2A のエージェントの中で使う道具を MCP サーバーとして切り出すと、同じ道具を Claude などの MCP 対応のホストからも使えます。

よくある質問

Q. A2A を使うには Google のサービスが要りますか。

要りません。仕様は Apache License 2.0 で公開され、運営は Linux Foundation の下の Agentic AI Foundation に移っています。この記事でも、Python の SDK と自分のマシンだけで動かしました。SDK は Python・JavaScript・Java・C#/.NET・Go・Rust が公式サイトに載っています。

Q. A2A と MCP はどちらか一方を選ぶものですか。

いいえ。MCP はエージェントが道具を使うため、A2A はエージェントが別のエージェントに頼むための仕様で、1 つのシステムで両方を使うのが普通です。公式ドキュメントも補い合う関係として説明しています。

Q. Agent Card はどこに置けばよいですか。

エージェントのドメインの /.well-known/agent-card.json です(RFC 8615 の well-known URI)。ほかに、カタログやレジストリに登録する方法と、呼ぶ側に直接設定する方法があります。/.well-known/agent.json は v0.3.0 より前の名前で、1.x の SDK は返しません。

Q. contextId と taskId の違いは何ですか。

taskId は 1 つの仕事、contextId は複数のタスクやメッセージをまとめる会話の単位です。終端になったタスクの続きを頼むときは、新しいタスクとして送り、同じ contextId を付けると関連する会話として扱えます。

Q. ストリーミングと push 通知はどちらを使えばよいですか。

呼ぶ側が結果を待っている間、接続を保てるならストリーミング(SSE)が簡単です。数分を超える処理や、呼ぶ側がサーバーレスで接続を保てない場合は push 通知にし、通知を受けたら GetTask で取り直します。どちらも使えない場合は GetTask のポーリングでも同じ情報が取れます。

Q. v0.3 のクライアントは v1.0 のサーバーと話せますか。

サーバーが互換を用意していれば話せます。1.3.0 の Python SDK では enable_v0_3_compat=True で、同じ URL が 0.3 の message/send を受け付けました。有効にしていなければ -32601 になります。逆に 1.3.0 のクライアントは、0.3 のカードを見て 0.3 の書式で送りました。

Q. JSON-RPC・REST・gRPC のどれを使えばよいですか。

機能は 3 つとも同じで、Agent Card の supportedInterfaces に対応するものを並べます。手元の計測では JSON-RPC と REST の差は誤差の範囲でした。ブラウザや curl で扱いやすいのは JSON-RPC か REST、社内のマイクロサービスが gRPC でそろっているなら gRPC が候補です(gRPC はこの環境では動かしていません)。

Q. 本番で使える段階ですか。

仕様は v1.0 で、Python SDK も 1.x が続けて公開されています(1.0.0 は 2026年4月20日、1.3.0 は 10月27日)。一方で、相手の製品が 0.3 と 1.0 のどちらに対応しているかは製品ごとに違うため、つなぐ相手のカードと公式ドキュメントで版を確かめてから設計します。

まとめ

  • A2A は、エージェント同士が Agent Card で能力を公開し、Task で仕事を頼み合うための公開仕様。MCP(エージェント↔ツール)と補い合い、2026年8月から AAIF の下で運営されている。
  • 仕様は v1.0。メソッド名は SendMessage・SendStreamingMessage・GetTask などに変わり、状態は TASK_STATE_COMPLETED、役割は ROLE_USER の形。A2A-Version: 1.0 を付けないと -32009 になる。
  • Agent Card は /.well-known/agent-card.json(v0.3.0 で agent.json から改名)。SDK 0.3.26 は両方を返し、1.3.0 は agent.json に 404 を返した。
  • 状態は進行中・中断(input-required・auth-required)・終端の 3 種類。SSE は終端か中断で閉じ、人の確認は同じ taskId に返事を送って再開する。確認待ちの内容は永続化する。
  • LLM が無くても、カード・タスク・SSE・成果物・エラーはすべて手元で確かめられる。プロトコルの手間は 1 回 4ms 前後で、採用は「相手を中まで変えられるか」で決める。

参考・出典

確認日はいずれも 2026年11月4日です。

  • A2A Protocol「Specification」(Latest Released Version 1.0.0):https://a2a-protocol.org/latest/specification/
  • A2A Protocol「What's New in v1.0」:https://a2a-protocol.org/latest/whats-new-v1/
  • A2A Protocol「Streaming & Asynchronous Operations」:https://a2a-protocol.org/latest/topics/streaming-and-async/
  • A2A Protocol「Enterprise-Ready Features」:https://a2a-protocol.org/latest/topics/enterprise-ready/
  • A2A Protocol「A2A and MCP」:https://a2a-protocol.org/latest/topics/a2a-and-mcp/
  • A2A Protocol トップ(運営・SDK の一覧):https://a2a-protocol.org/latest/
  • GitHub a2aproject/A2A「Releases」(v0.3.0・v1.0.0・v1.0.1):https://github.com/a2aproject/A2A/releases
  • PyPI「a2a-sdk」(1.3.0):https://pypi.org/project/a2a-sdk/
  • GitHub a2aproject/a2a-python:https://github.com/a2aproject/a2a-python
  • Google Cloud 公式ブログ(2025年4月10日、A2A の発表と設計原則):https://cloud.google.com/blog/ja/products/ai-machine-learning/a2a-a-new-era-of-agent-interoperability
  • Agentic AI Foundation「A2A joins AAIF」:https://aaif.io/blog/a2a-joins-aaif
  • Model Context Protocol「Specification」(2026-07-28):https://modelcontextprotocol.io/specification/latest

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「A2Aプロトコルとは|MCPとの違いとv1.0の通信を実物で確認」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

成長のヒミツ、 ぜんぶ丸わかり