A2A Java SDK(a2a-java)は、AIエージェント同士をつなぐAgent2Agent(A2A)プロトコルの公式Java実装です。この記事では、2026年11月3日公開の1.5.0.Finalを使い、QuarkusのA2AサーバーとJavaクライアントを書いて手元で動かした結果を載せます。2025年に書かれた解説の多くは0.2〜0.3系のままで、1.xではコンパイルできません。移行で変わった点と、既定設定でつまずく箇所も扱います。
まとめ:A2A Java SDKの要点
- 公式SDKはGitHubの a2aproject/a2a-java(Apache-2.0)です。1.0.0.Final(2026年6月10日)が初のGA版で、最新は1.5.0.Final(2026年11月3日)です。Java 17以上とMaven 3.8以上が要ります。
- サーバーは、エージェントの名刺にあたるAgentCardと、処理本体のAgentExecutorの2つのBeanで動きます。LLMを使うなら、LangChain4jのAIサービスをAgentExecutorから呼びます。
- 1.3.0から認可は既定で拒否(fail-closed)です。認可プロバイダを置かないと、新規の
SendMessageも-32001 Task not foundで返ります。ひとりで試すときだけa2a.authorization.required=falseを設定します。 - 1.2.0〜1.3.1には、認可の素通りやSSRFなど5件の脆弱性勧告が出ています。使うなら1.3.2.Final以上、新規なら1.5.0.Finalにします。
- 0.3系から上げると、
groupIdとパッケージがorg.a2aproject.sdkに変わり、EventQueue・TaskUpdaterはAgentEmitterに置き換わります。Google ADK for Java(1.14.0)は2026年11月5日時点でも0.3系に依存しています。
A2A Java SDKの位置づけと版の履歴
A2Aは、別々のフレームワークや言語で作られたエージェントが、互いの能力を調べて仕事を頼み合うための通信規約で、Linux Foundationのプロジェクトとして運営されています。仕様そのものやMCPとの違いはA2A(Agent2Agent)プロトコルとは?v1.0の仕組み・MCPとの違い・セキュリティ対策と実装手順で解説しているので、ここではJava実装に絞ります。
A2A Java SDKは、Red HatのQuarkusチームとWildFlyチームがGoogleと協力して立ち上げました。2025年の0.2系・0.3系を経て、A2A仕様1.0.0に合わせた1.0.0.FinalからGAです。その後はおよそ月1回のペースで出ています(日付はGitHubのリリース一覧の公開日)。
| 版 | 公開日 | 主な変更 |
|---|---|---|
| 1.0.0.Final | 2026-06-10 | 初のGA版。A2A仕様1.0.0に準拠 |
| 1.1.0.Final | 2026-06-29 | タスク単位の認可SPI(既定は許可) |
| 1.2.0.Final | 2026-08-07 | 認可の強化、ストリームのフック |
| 1.3.0.Final | 2026-08-27 | マルチテナント、認可の既定を拒否に |
| 1.3.2.Final | 2026-09-08 | Android版HTTPクライアントの修正 |
| 1.4.0.Final | 2026-09-28 | SSE解析の上限、RESTの新メディアタイプ |
| 1.5.0.Final | 2026-11-03 | HTTPクライアント設定の統合、リダイレクト |
1.5.0.Finalのリリースノートでは、互換性を壊す変更が2件告知されています。1件はサーバー側の入力検証の一元化で、transportのハンドラーを直接使う統合ライブラリ向けです。PRには「利用者向けの公開APIは変わらない」とあり、実際に1.4.0.Final用に書いたサーバーとクライアントは、ソースを変えずに1.5.0.Finalでビルドと実行が通りました。もう1件はHTTPクライアントの設定を A2AHttpClientConfiguration にまとめた変更で、JDKの HttpClient を自前で渡している場合に影響します(後述)。
依存関係の選び方:BOMとトランスポート
座標はすべて org.a2aproject.sdk です。BOMは3種類あり、Quarkusで作るなら参照実装用を読み込みます。
| BOM | 使う場面 |
|---|---|
a2a-java-sdk-bom |
Spring Bootや素のJavaで組む |
a2a-java-sdk-extras-bom |
DB永続化・複数台構成を足す |
a2a-java-sdk-reference-bom |
Quarkusの参照実装を使う |
サーバーはトランスポートごとに a2a-java-sdk-reference-jsonrpc・-grpc・-rest を選び、複数を同時に入れられます。クライアントは a2a-java-sdk-client にJSON-RPCが含まれ、gRPCとRESTは a2a-java-sdk-client-transport-grpc・-rest を足します。検証に使ったサーバーのpom.xmlの要点は次のとおりです。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.a2aproject.sdk</groupId>
<artifactId>a2a-java-sdk-reference-bom</artifactId>
<version>1.5.0.Final</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.a2aproject.sdk</groupId>
<artifactId>a2a-java-sdk-reference-jsonrpc</artifactId>
</dependency>
</dependencies>
<!-- build の quarkus-maven-plugin は 3.40.1.1(SDK 1.5.0の親pomが使う版)に揃える -->
プラグインの版は、SDKの親pomにある quarkus.platform.version に合わせます。BOMが持ち込むQuarkus本体は3.40.1.1なので、プラグインだけ古い3.39.2のままでも起動はしましたが、ビルド時と実行時でQuarkusの版がずれた状態になります。importやファイル配置を含む完全な構成は、公式リポジトリの examples/helloworld(Quarkusのサーバーと、それを呼ぶクライアント)が参考になります。検証では mvn package でビルドし、java -jar target/quarkus-app/quarkus-run.jar で起動しました。
A2Aサーバーの実装手順(Quarkus)
サーバー側で書くのは、AgentCardを返すBeanと、AgentExecutorを返すBeanの2つです。まず、受け取った文字列を大文字にして返すだけのエージェントで仕組みを確かめ、次にLLMを呼ぶ形へ広げます。動作確認はTemurin 25.0.5とQuarkus 3.40.1.1で行いました。以下の接続先に合わせ、サーバーの src/main/resources/application.properties に quarkus.http.port=9999 を設定します。
AgentCardの定義と公開先
@PublicAgentCard を付けたAgentCardは /.well-known/agent-card.json で公開されます。supportedInterfaces に、どの方式でどのURLに接続できるかを書きます。
@ApplicationScoped
public class AgentCardProducer {
@Produces
@PublicAgentCard
public AgentCard agentCard() {
return AgentCard.builder()
.name("Upper Agent")
.description("受け取ったテキストを大文字にして返す")
.supportedInterfaces(List.of(
new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999")))
.version("1.0.0")
.capabilities(AgentCapabilities.builder().streaming(true).pushNotifications(false).build())
.defaultInputModes(List.of("text"))
.defaultOutputModes(List.of("text"))
.skills(List.of(AgentSkill.builder()
.id("upper")
.name("Uppercase")
.description("テキストを大文字に変換する")
.tags(List.of("text"))
.examples(List.of("hello a2a"))
.build()))
.build();
}
}
起動後に curl http://localhost:9999/.well-known/agent-card.json で取得すると、上の内容がJSONで返り、応答ヘッダーには Cache-Control: public, max-age=3600 が付きました。キャッシュ時間は a2a.agent-card.cache.max-age で変えられます。認証済みの相手にだけ見せたい能力は @ExtendedAgentCard 側に書き、GetExtendedAgentCard で返します。SDKはこのメソッドを個別に認証しないので、Quarkus Securityなどで認証をかけてください。
AgentExecutorとAgentEmitterによるタスク処理
AgentExecutorの execute が処理の本体で、AgentEmitter を通じてタスクの状態とアウトプット(Artifact)を送ります。
@ApplicationScoped
public class AgentExecutorProducer {
@Produces
public AgentExecutor agentExecutor() {
return new AgentExecutor() {
@Override
public void execute(RequestContext context, AgentEmitter emitter) throws A2AError {
if (context.getTask() == null) {
emitter.submit(); // 新規タスクとして受け付ける
}
emitter.startWork(); // TASK_STATE_WORKING へ
StringBuilder sb = new StringBuilder();
for (Part<?> part : context.getMessage().parts()) {
if (part instanceof TextPart text) {
sb.append(text.text());
}
}
emitter.addArtifact(List.of(new TextPart(sb.toString().toUpperCase())));
emitter.complete(); // TASK_STATE_COMPLETED へ
}
@Override
public void cancel(RequestContext context, AgentEmitter emitter) throws A2AError {
emitter.cancel();
}
};
}
}
ストリーミングで呼ぶと、この順序がそのままSSEのイベントになります。実測では TASK_STATE_SUBMITTED → TASK_STATE_WORKING → Artifactの更新 → TASK_STATE_COMPLETED の4件が届きました。キャンセルできない状態のタスクには TaskNotCancelableError を投げます。
LangChain4jによるLLM呼び出しとタイムアウト設定
Quarkusの公式ブログや多くのチュートリアルは、Quarkus LangChain4jのAIサービスをA2Aで公開する構成を取ります。1.xでも形は同じで、AIサービスをAgentExecutorに注入し、戻り値を addArtifact に渡します。依存に io.quarkiverse.langchain4j:quarkus-langchain4j-openai の1.14.2を足し、次のインターフェースを置きます。
@RegisterAiService
public interface Summarizer {
@SystemMessage("受け取った文章を日本語で3行に要約してください。")
String summarize(@UserMessage String text);
}
上のAgentExecutorProducerに @Inject Summarizer summarizer; を足し、toUpperCase() を含む行全体を emitter.addArtifact(List.of(new TextPart(summarizer.summarize(sb.toString())))); に置き換えます。APIキーは quarkus.langchain4j.openai.api-key、接続先は quarkus.langchain4j.openai.base-url で指定します。接続先をローカルに立てたOpenAI互換のモックへ向けて SendMessage を送ると、システムメッセージとユーザーの文章がモデルへ渡り、モックの応答がArtifactに入って TASK_STATE_COMPLETED で返りました。モデル名を省くと、リクエストには既定の gpt-4o-mini が入っていたので、本番では quarkus.langchain4j.openai.chat-model.model-name を明示します。
LLMを呼ぶときは、2つのタイムアウトが重なる点に注意してください。Quarkus LangChain4jのOpenAI呼び出しは既定10秒(quarkus.langchain4j.openai.timeout)、A2A側の同期呼び出しでエージェントの完了を待つ時間は既定30秒(a2a.blocking.agent.timeout.seconds)です。長い生成では先に10秒の側で失敗します。SDKの設定ガイドはLLMを使うエージェントに60〜120秒を勧めているので、LLM呼び出し側のタイムアウトを生成時間に合わせて伸ばします。そのうえで、A2A側も同期呼び出しの待機時間を伸ばすか、ストリーミング受信に切り替えます。
既定のfail-closed認可でリクエストが拒否される理由
AgentCardとAgentExecutorだけで起動し、JSON-RPCで SendMessage を送ると、エージェントは呼ばれず次のエラーが返ります。1.5.0.Finalでも同じでした。
curl -s -X POST http://localhost:9999/ \
-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":"hello a2a"}],"messageId":"m-1"}}}'
{"jsonrpc":"2.0","id":1,"error":{"code":-32001,"message":"Task not found", ...}}
原因は、1.3.0で認可の既定が「プロバイダが無ければ全操作を拒否」に変わったことです。拒否はタスクの存在を推測させないよう Task not found として返るため、エラー文から認可の問題だと気づきにくくなっています。ストリーミングの SendStreamingMessage でも、SSEの最初のイベントとして同じ -32001 が届きました。ひとりで試すときは application.properties に次の1行を足します。
a2a.authorization.required=false
この設定で再起動すると、同じリクエストに "state":"TASK_STATE_COMPLETED" と "text":"HELLO A2A" を含む結果が返りました。ただし、この設定はプロバイダが無いときの拒否を外すだけで、プロバイダを置けばそちらの判定が常に優先されます(認可ガイド)。複数の利用者が使う本番では、この設定に頼らず TaskAuthorizationProvider を実装します。新規タスクは checkCreate で受け付けて recordOwnership で作成者を記録し、既存タスクは checkRead・checkWrite で作成者と呼び出し元が一致するかを判定します。認証基盤の候補はKeycloakとは?Oktaとの違いと連携手順|Auth0・Cognito比較で導入を判断【2026年10月】で比較しています。
Javaクライアントの実装:AgentCard取得からストリーミング受信まで
クライアントはAgentCardを取得し、その内容から Client を組み立てます。受け取ったイベントは登録したコンシューマに届きます。
AgentCard card = A2ACardResolver.builder()
.baseUrl("http://localhost:9999")
.build()
.getAgentCard();
CountDownLatch done = new CountDownLatch(1); // ストリーム終了を待つ
List<BiConsumer<ClientEvent, AgentCard>> consumers = List.of((event, c) -> {
if (event instanceof TaskUpdateEvent u) {
System.out.println("update: " + u.getTask().status().state());
}
});
try (Client client = Client.builder(card)
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig())
.addConsumers(consumers)
.streamingErrorHandler(e -> {
if (e != null) e.printStackTrace(); // null は正常終了の合図
done.countDown();
})
.build()) {
client.sendMessage(A2A.toUserMessage("hello a2a")); // 送信後すぐ戻る
if (!done.await(30, TimeUnit.SECONDS)) {
throw new java.util.concurrent.TimeoutException("A2Aストリームが30秒以内に終了しませんでした");
}
}
サーバーがストリーミングに対応していれば、sendMessage は自動でストリーミングを使い、応答を待たずに戻ります。main から呼ぶなら、上のように CountDownLatch などで終わりを待ちます。待たないと、イベントが届く前にJVMが終了します。1.5.0.Finalで実行すると、TaskUpdateEvent が4回(SUBMITTED、WORKING、Artifact追加時のWORKING、COMPLETED)届き、最後のタスクに HELLO A2A のArtifactが入っていました。streamingErrorHandler は正常終了のときにも引数 null で1回呼ばれます。ソースの AbstractSSEEventListener にも null to signal normal completion と書かれているので、null判定を入れないとエラー処理が誤って動きます。
既定のHTTPクライアントはリダイレクトを追いません。認証情報が別のドメインへ転送されるのを防ぐためです。1.5.0では、リクエストごとに followRedirects(true) を付けると1回だけ追えるようになり、AgentCardの取得ではこれが自動で有効です。ストリーミングの通信はリダイレクトを追いません。自前の HttpClient を JdkA2AHttpClient に渡す場合は、JDK側のリダイレクト設定を既定の NEVER のままにします。それ以外を設定すると、followRedirects(true) を付けたリクエストは IOException で拒否されます。APIキーを送るヘッダー名も、Authorization・X-API-Key など5種類の許可リストに無ければ付与されません。
脆弱性勧告と上げるべき版
A2A Java SDKには、1.2.0.Final〜1.3.1.Finalを対象とするGitHubセキュリティアドバイザリが5件出ています。いずれも1.3.0.Finalか1.3.2.Finalで修正済みです。
| 勧告 | 深刻度 | 内容 | 修正版 |
|---|---|---|---|
| GHSA-qw47-mcm5-934w | High | タスク認可の既定が許可 | 1.3.0.Final |
| GHSA-q78c-5jjq-57g8 | Medium | プッシュ通知送信のSSRF | 1.3.0.Final |
| GHSA-9rhm-2h4x-jwmx | Medium | 相手指定のヘッダー名で認証情報を送信 | 1.3.0.Final |
| GHSA-x32g-jvvm-4725 | Medium | 通知用の認証情報をログに平文出力 | 1.3.0.Final |
| GHSA-4fpj-g9jf-9rgg | High | Android版POSTが別ホストへ認証情報を転送 | 1.3.2.Final |
上の認可の既定変更とヘッダー名の許可リストは、この勧告への対応として入ったものです。認可プロバイダを置かずに1.1〜1.2系で動かしているサーバーは、上げただけで拒否側に切り替わり、それまで通っていたリクエストが -32001 になります。先に TaskAuthorizationProvider を用意してから上げてください。0.3系(io.github.a2asdk)は2026年11月3日に保守版の0.3.4が出ていますが、Maven上の版名は .Final の付かない 0.3.4 です。
0.3系の記事やサンプルから移行するときの差分
「A2A Java」で見つかるチュートリアルの多くは2025年の0.2〜0.3系向けです。0.3.3.Finalのjarと1.x のソースを照合した差分は次のとおりです。
| 項目 | 0.3系 | 1.x |
|---|---|---|
| groupId | io.github.a2asdk |
org.a2aproject.sdk |
| パッケージ | io.a2a.* |
org.a2aproject.sdk.* |
| 処理の引数 | EventQueue + TaskUpdater |
AgentEmitter |
| 値の取得 | task.getStatus() |
task.status()(record) |
| JSON-RPCメソッド | message/send |
SendMessage |
| AgentCardの接続先 | url + preferredTransport |
supportedInterfaces |
1.5.0のサーバーに対して確かめると、A2A-Version ヘッダーを省いたリクエストは0.3扱いになり -32009 Protocol version '0.3' is not supported で拒否され、旧メソッド名の message/send は -32601 でした。params を省いた SendMessage は -32602 Parameter 'message' may not be null です。古いクライアントを残したまま移行するなら、a2a-java-sdk-reference-multiversion-jsonrpc(RESTは -multiversion-rest)を使うと、ヘッダーが 1.0 なら1.0、0.3 か省略なら0.3の処理系へ振り分けられ、AgentExecutor は書き換えずに済みます。この場合はAgentCardのビルダーに、0.3のクライアントが見る url・preferredTransport・additionalInterfaces(Legacy_0_3_AgentInterface を渡す)も設定します。0.3系のエージェントを呼ぶ側は、a2a-java-sdk-compat-0.3-client の Client_v0_3 を使います。
Spring Boot・Google ADKと組み合わせるときの選択肢
公式SDKの参照実装はQuarkusで、Jakarta EE向けにはWildFlyチームの a2a-jakarta があります。Spring Bootで使う選択肢は、2026年11月時点で次の4つです。
| 選択肢 | 最新版 | 依存するA2A SDK |
|---|---|---|
| 公式SDKを手で配線 | 1.5.0.Final | 1.x |
| spring-ai-a2a | 0.4.0(2026-10-23) | 1.4.0.Final |
| a2a-spring | 0.1.0-SNAPSHOT | 1.x |
| google-adk-a2a | 1.14.0(2026-11-02) | 0.3.2.Final |
- 公式SDKを手で配線する:サーバーガイドに、
MainEventBus・DefaultRequestHandlerなどをSpringのBeanとして登録する例があります。手で組んだハンドラーにはa2a.blocking.*の設定が効かないので、タイムアウトはビルダーのagentCompletionTimeoutSeconds(int)などで指定します。 - spring-ai-a2a:Spring AIのChatClientをA2Aサーバーとして公開するコミュニティ製の統合です。0.4.0でSpring Boot 4.1.1・Spring AI 2.0.1へ上がり、A2A 1.0のJSON-RPCをSDKの処理系で受けるようになりました。READMEのサンプルは旧APIの書き方を含むため、依存の版番号だけでなく、AgentCardのビルダーや接続先の定義、JSON-RPCリクエストも1.x向けに修正します。Spring AI自体の組み込みはSpring AIの使い方:Spring Bootへの組み込み手順と2.0の変更点を参照してください。
- a2a-spring:公式のサーバーガイドに1.5.0から載ったSpring MVC向けの統合です。READMEは自らを初期のMVPとしており、Maven Centralには未公開なので、本番では採用を見送るべきです。
- Google ADK for Java:
com.google.adk:google-adk-a2aでADKのエージェントをA2Aで公開できますが、1.14.0のpomが依存しているのは旧座標の0.3.2.Finalです。ADK自体はADKとは?GoogleのAgent Development Kitの仕組みと始め方【2.11対応】で紹介しています。
新規に作るなら、公式SDKの1.xか、それに乗ったspring-ai-a2a 0.4.0を選びます。ADKなど0.3系に依存する統合から1.0専用のエージェントを呼ぶと、message/send が通らず -32601 で弾かれます。相手が0.3系なら、受ける側は互換モジュールで両方の版を受け付け、呼ぶ側は Client_v0_3 を使います。言語をまたいだフレームワーク選びはAIエージェントフレームワークとは?主要8種の比較・動作コード・選定基準【2026年9月版】で比べています。業務システムへのエージェント組み込みを外部に相談するなら、一創のAIエージェント開発でも設計から支援しています。
本番運用で差し替えるコンポーネント
既定のタスクストアとキューはメモリ上にあり、再起動で消え、複数台では共有されません。extrasモジュールを足して接続先を設定すれば、AgentExecutor を変えずに差し替わります。
| 用途 | モジュール |
|---|---|
| タスクの永続化 | a2a-java-extras-task-store-database-jpa |
| 複数台でのイベント共有 | a2a-java-queue-manager-replicated-core ほか |
| 分散トレース | a2a-java-sdk-opentelemetry-server |
| マルチテナント | a2a-java-extras-multitenancy |
1.5.0では、JPAのタスクストアに所有者を保存する JpaTaskAuthorizationProvider が加わりました。モジュールを入れただけでは有効にならず、Quarkusでは quarkus.arc.selected-alternatives で明示的に選びます。タスク一覧の取得で、他人のタスクを除いた件数とページをDB側で絞り込めるようになります。OpenTelemetryのモジュールも、ストリーミングとプッシュ通知の区間を記録するようになりました。トレースの基盤はOpenTelemetryとは?仕組み・Collectorの導入手順とPrometheusとの違い【2026年10月】で解説しています。
複数台構成のイベント共有は、replicated-core に a2a-java-queue-manager-replication-mp-reactive を組み合わせ、Kafka・Pulsar・AMQPのいずれかを使います。プッシュ通知の送り先URLは既定で https のみ、社内ネットワーク宛ては拒否されます(上のSSRF勧告への対策)。開発中に http://localhost へ送りたいときだけ、a2a.push-notification.url.allowed-schemes=http,https と a2a.push-notification.url.allow-private-network-targets=true を設定します。エージェントをいくつに分けるべきかはマルチエージェントとは?仕組み・構成パターンと採用判断を研究データで解説【2026年10月】で扱っています。
よくある質問
A2A Java SDKはJava 11で動きますか?
1.x はJava 17以上が必要です。SDKのビルド設定も release 17で、入門ガイドの要件はJava 17以上とMaven 3.8以上です。
Python製のA2Aエージェントと通信できますか?
できます。A2Aは言語に依存しない規約で、公式のHello Worldサンプルも、JavaサーバーをPythonクライアントから呼ぶ構成です。双方が同じプロトコル版(1.0か0.3)を話しているかを先に確かめてください。
「A2Aバス」や「sendEvent」というAPIはありますか?
公式SDKにはありません。1.xの公開APIは、サーバー側が AgentExecutor と AgentEmitter、クライアント側が Client の sendMessage・getTask・cancelTask などです。内部の MainEventBus は、イベントをタスクストアへ保存してから配信する仕組みで、エージェント間のメッセージバスではありません。
gRPCとJSON-RPCのどちらを選べばよいですか?
SDKは3方式を同格に扱い、依存関係を並べれば同時に公開できます。ブラウザや他言語のクライアントから呼ぶならJSON-RPCかREST、社内のJava同士で型付きの通信をしたいならgRPCが向きます。
MCPとA2Aは両方必要ですか?
役割が違います。MCPはエージェントが外部ツールやデータを呼ぶための規約で、A2Aはエージェント同士が仕事を頼み合うための規約です。ツール接続の実装はMCPサーバーの作り方とAIエージェントへの接続手順|Python SDK 2.3でツール定義・認可を実装で解説しています。