- AI ⚙️
A2Aプロトコルとは|MCPとの違いとv1.0の通信を実物で確認
結論
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 通知・認証の要点、採用の判断表
この記事で実際に動かしたもの: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 で返される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 が受け持ちます。
venv に SDK を入れる
pip install "a2a-sdk[http-server]==1.3.0" uvicorn。http-server の extra で Starlette と sse-starlette が入る。キャッシュを含めて 65MB でした。
AgentExecutor を書く
計算エージェントは Task を作って working → 成果物 → completed。受付エージェントは Message を 1 つ返すだけ。
Agent Card を作る
スキル・supportedInterfaces(JSON-RPC と REST の URL)・capabilities.streaming を書く。
ルートを組み立てて起動
カード・JSON-RPC・REST のルートを Starlette に渡し、uvicorn で 127.0.0.1 の空きポートに立てる。
計算エージェントの中心部分です(式の評価は 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 のルート) |
|---|---|---|
| 送信 | SendMessage | POST /message:send |
| 逐次の送信 | SendStreamingMessage | POST /message:stream |
| タスク取得 | GetTask | GET /tasks/{id} |
| 一覧 | ListTasks | GET /tasks |
| 取り消し | CancelTask | POST /tasks/{id}:cancel |
| 再購読 | SubscribeToTask | GET または POST /tasks/{id}:subscribe |
| push 通知の設定 | Create・Get・List・Delete の 4 つ | /tasks/{id}/pushNotificationConfigs と /{push_id} |
| 拡張カード | GetExtendedAgentCard | GET /extendedAgentCard |
固定応答の受付エージェントに同じマシンから 300 回ずつ送った所要時間です。プロトコルの手間は数ミリ秒で、実際の所要はエージェントの処理(LLM の推論など)で決まります。
タスクの状態遷移(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 の値 | 種類 | この環境で起こした方法 |
|---|---|---|---|
| submitted | TASK_STATE_SUBMITTED | 進行中 | returnImmediately・SSE の最初のイベント |
| working | TASK_STATE_WORKING | 進行中 | start_work() |
| input-required | TASK_STATE_INPUT_REQUIRED | 中断 | 20 文字を超える式 |
| auth-required | TASK_STATE_AUTH_REQUIRED | 中断 | (公式仕様の範囲) |
| completed | TASK_STATE_COMPLETED | 終端 | 計算できた |
| failed | TASK_STATE_FAILED | 終端 | 1/0(division by zero) |
| canceled | TASK_STATE_CANCELED | 終端 | 確認に「いいえ」・CancelTask |
| rejected | TASK_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)で行います。
確認待ちの内容をどこに持つか
判断:試作は 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":{…}} | 呼ぶ側のプログラムが読む結果・フォームの値 |
| ファイルの場所 | url | file.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 が効くのは「相手のエージェントを自分たちで中まで変えられない」ときです。次の順に考えると判断がぶれません。
- 11 つのプロセス・1 つのチームで完結する → A2A は要らないフレームワーク内のサブエージェントや関数呼び出しで済む。A2A にすると状態の管理と HTTP の往復が増えるだけ
- 2相手が入出力の決まった道具 → MCP検索・DB・ファイル・社内 API。呼ぶ側の LLM が使い方を決める
- 3相手が別チーム・別会社のエージェントで、中を見せない → A2AAgent Card で能力を知り、タスクで頼む。言語やフレームワークを合わせなくてよい
- 4処理が長い・途中で人の確認や追加の認証が要る → A2A のタスクinput-required・auth-required・SubscribeToTask・push 通知がそのまま使える
- 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期連続の黒字決算。