0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaでAgentをA2A通信させてみる

Posted at

はじめに

生成AI界隈が賑わっている昨今、AgentというAIが自律して依頼をこなす仕組みが実現できるようになり、今年4月にAgent同士が通信するA2AというプロトコルがGoogleから登場した。

現在(2025年9月)はLinux Foundationに寄贈され、v0.3.0がリリースされている状態で日々アップデートされて続けているが、Pythonだけでなく様々なプログラミング言語でのサンプルも提供されているので、普段からJavaを触っていてPythonはほとんど触ったことのない自分のようなエンジニアでも参考程度にA2AをJavaで体験してみることにした。

対象読者

  • Agent開発に興味がある人
  • Javaが得意でPythonが苦手な人
  • Gradleが好きでMavenが嫌いな人
  • Quarkusに興味がある人

前提知識

Google ADKを知っていること

A2Aで通信対象となるAgentは何で開発されていても良い(例:OpenAI Agents SDK、LangChain、Mastraなど)が、今回はAgentの開発もJavaで行うのでGoogle ADKを使うことにする。なので、GoogleADKのJavaでAgentを開発できる知識があることを前提とする。

LLMにGemini APIを使用するので、事前にGOOGLE_API_KEYを取得してください。

Quarkusを知っていること

A2AはJSON-RPCな通信プロトコルなので当然Webサービスが必要になる。
JavaでWebサービスを提供するAPサーバは色々ある(Tomcat/Jetty/WildFly/Consminexusなど)が、A2A公式からはQuarkusでのリファレンス実装が提供されているため、それ以外のAPサーバ(SpringBootなど)でA2Aを体験する場合はQuarkusのリファレンス実装を参考に、そのAPサーバ用の実装をしなければならない。

なので、今回は公式からリファレンス実装が提供されているQuarkusを前提とする。

実装

プロジェクト雛形作成

Mavenは嫌いなので公式でサンプルが提供されているので、今回はGradleで試すことにした。

$ mkdir sample-a2a-quarkus
$ cd sample-a2a-quarkus
$ gradle init --type java-application
$ tree
.
├── app
│   ├── build.gradle.kts
│   └── src
│       ├── main
│       │   ├── java
│       │   │   └── org
│       │   │       └── example
│       │   │           └── App.java
│       │   └── resources
│       └── test
│           ├── java
│           │   └── org
│           │       └── example
│           │           └── AppTest.java
│           └── resources
├── gradle
│   ├── libs.versions.toml
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts

Google ADKでAgent開発

A2Aする前に、まずGoogleADKで開発したAgentをQuarkusで動かすところまでを実装する。
GoogleADKはAgentだけでなくRunnerSessionServiceの組み合わせてRuntimeを構成するので、それらをQuarkusでインメモリに保持するシンプルな構成とする。

$ tree app/
app/
├── build.gradle.kts  ## ここを編集
└── src
    ├── main
    │   ├── java
    │   │   └── org
    │   │       └── example
    │   │           └── SampleAgent.java  ## ここを追加
    │   └── resources
    └── test
        ├── java
        │   └── org
        │       └── example
        └── resources

Gradle設定

Quarkusプラグインと、GoogleADKなどの依存を追加する。

app/build.gradle.kts
plugins {
    application
    id("io.quarkus") version "3.26.3" // 追加
}

//中略

dependencies {
    implementation(enforcedPlatform("io.quarkus.platform:quarkus-bom:3.26.3")) // 追加
    implementation("io.quarkus:quarkus-rest") // 追加
    implementation("com.google.adk:google-adk:0.2.0") // 追加
}

// 以下略

Javaコーディング

JAX-RSで /chat にAgentの動作確認ができるAPIを公開する。

app/src/main/java/org/example/SampleAgent.java
@Path("/chat")
public class SampleAgent {
    private final BaseAgent agent = LlmAgent.builder()
            .model("gemini-2.5-flash")
            .name("my-agent")
            .description("役立つエージェント")
            .instruction("あなたは役立つエージェントです")
            .build();
    private final Runner runner = new InMemoryRunner(agent);
    private final Session session = runner
            .sessionService()
            .createSession(agent.name(), "sample-user-id")
            .blockingGet();

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String chat(@QueryParam("prompt") String prompt) {
        String userId = session.userId();
        Content userMsg = Content.fromParts(Part.fromText(prompt));
        Flowable<Event> events = runner.runAsync(userId, session.id(), userMsg);
        // イベントストリームからの出力を結合
        StringBuilder outputBuilder = new StringBuilder();
        events.blockingForEach(event -> outputBuilder.append(event.stringifyContent()));
        return outputBuilder.toString();
    }
}

動作確認

$ export GOOGLE_API_KEY="取得したAPIキー"
$ ./gradlew clean quarkusDev

$ curl http://localhost:8080/chat?prompt=hello
> Agentからの回答がレスポンスされるはず

A2Aの実装

上記でRestAPIでAgentとチャットできるようになったと思うので、いよいよA2A Java SDKを使ってA2Aを実装していく。
今回はQuarkusでA2Aサーバ側を実装し、junitのテストコードでA2Aのクライアント側を実装して、テストでA2A通信がうまく行くことを検証する。

なお、現時点(2025/9)でリリースされているA2Aバージョン0.3.0を対象とするが、他バージョンとの互換性は保証できないし、A2A Java SDKはまだBeta版しかリリースされていない状態での検証であることをご留意いただきたい。

依存の追加

JAX-RSでの提供を廃止するのでquarkus-restを削除し、A2Aに必要な依存を追加する。

app/build.gradle.kts
dependencies {
    implementation(enforcedPlatform("io.quarkus.platform:quarkus-bom:3.26.3"))
    // implementation("io.quarkus:quarkus-rest") // 削除してよい
    implementation("com.google.adk:google-adk:0.2.0")
    implementation("io.github.a2asdk:a2a-java-sdk-reference-jsonrpc:0.3.0.Beta1") // 追加

    testImplementation("io.quarkus:quarkus-junit5") // 追加
    testImplementation("io.github.a2asdk:a2a-java-sdk-client:0.3.0.Beta1") // 追加
}

JAX-RSを廃止

先程実装したJAX-RS実装を削除する

app/src/main/java/org/example/SampleAgent.java
@ApplicationScoped  //追加
//@Path("/chat") // 削除
public class SampleAgent {
    private final BaseAgent agent = LlmAgent.builder()
            .model("gemini-2.5-flash")
            .name("my-agent")
            .description("役立つエージェント")
            .instruction("あなたは役立つエージェントです")
            .build();
    private final Runner runner = new InMemoryRunner(agent);
    private final Session session = runner
            .sessionService()
            .createSession(agent.name(), "sample-user-id")
            .blockingGet();

//    @GET // 削除
//    @Produces(MediaType.TEXT_PLAIN) // 削除
    public String chat(
        // @QueryParam("prompt")  // 削除
        String prompt
    ) {
        String userId = session.userId();
        Content userMsg = Content.fromParts(Part.fromText(prompt));
        Flowable<Event> events = runner.runAsync(userId, session.id(), userMsg);
        // イベントストリームからの出力を結合
        StringBuilder outputBuilder = new StringBuilder();
        events.blockingForEach(event -> outputBuilder.append(event.stringifyContent()));
        return outputBuilder.toString();
    }
}

AgentCardの提供

A2Aプロトコルのお約束として、AgentCardというjsonを提供しなければならない。(AgentCardの詳細はこちらを参照)

A2Aの初歩的な通信ができるところまでを検証するには必要最低限のAgentCardでよいが、公式GitHubから提供されている helloworld を参考にAgentCardを提供することにした。
https://github.com/a2aproject/a2a-java/blob/main/examples/helloworld/server/src/main/java/io/a2a/examples/helloworld/AgentCardProducer.java

app/src/main/java/org/example/SampleAgentCardProducer.java
@ApplicationScoped
public class SampleAgentCardProducer {

    @Produces
    @PublicAgentCard    // /.well-known/agent-card.json でAgentCardをGETできるようになる
    public AgentCard agentCard() {
        return new AgentCard.Builder()
                .name("my-agent")
                .description("役立つエージェント")
                .url("http://localhost:8080") // A2A提供エンドポイント
                .version("1.0.0")
                .documentationUrl("http://example.com/docs")
                .capabilities(new AgentCapabilities.Builder()
                        .streaming(true)
                        .pushNotifications(true)
                        .stateTransitionHistory(true)
                        .build())
                .defaultInputModes(Collections.singletonList("text"))
                .defaultOutputModes(Collections.singletonList("text"))
                .skills(Collections.singletonList(new AgentSkill.Builder()
                        .id("hello_world")
                        .name("Returns hello world")
                        .description("just returns hello world")
                        .tags(Collections.singletonList("hello world"))
                        .examples(List.of("hi", "hello world"))
                        .build()))
                .protocolVersion("0.3.0")  // A2A バージョン
                .build();
    }
}

AgentExecutorの提供

A2Aのサーバ側で受信したリクエストを処理して、A2Aのレスポンスを返すまでのロジックはAgentExecutorで実行される必要があり、このinterfaceAgentExecutorが提供されているので具象クラスを作成する。

app/src/main/java/org/example/SampleAgentExecutorProducer.java
@ApplicationScoped
public class SampleAgentExecutorProducer {

    @Inject
    SampleAgent sampleAgent; // Google ADKで作成したAgentを注入

    @Produces
    public AgentExecutor agentExecutor() {
        return new SampleAgentExecutor(sampleAgent);
    }

    private static class SampleAgentExecutor implements AgentExecutor {

        private final SampleAgent agent; // 注入されたGoogleADKのAgentをプライベート変数で参照できるようにする
        public SampleAgentExecutor(SampleAgent agent) { this.agent = agent; }

        /**
         * A2Aサーバで受信したメッセージを処理する
         *
         * @param context    A2Aサーバの受信データ
         * @param eventQueue このキューに追加するとA2Aでクライアントにイベントが通知される
         * @throws JSONRPCError
         */
        @Override
        public void execute(RequestContext context, EventQueue eventQueue) throws JSONRPCError {
            /** このupdaterでtaskの状態を遷移させる。遷移すると内部でEventQueueにenqueueされてクライアントに通知される */
            TaskUpdater updater = new TaskUpdater(context, eventQueue);
            if (context.getTask() == null) {
                updater.submit(); // SUBMITTED状態に遷移
            }
            updater.startWork(); // WORKING状態に遷移

            String userMessage = this.extractTextFromMessage(context.getMessage()); // 受信したメッセージを結合
            String response = agent.chat(userMessage);  // 受信したメッセージをGoogle ADKで作成したAgentに渡して回答を待つ

            // Agentの回答からA2Aの返信メッセージを作る
            TextPart responsePart = new TextPart(response, null);
            List<Part<?>> parts = List.of(responsePart);

            // 成果物(アーティファクト)に追加
            updater.addArtifact(parts, null, null, null);
            updater.complete();
        }

        @Override
        public void cancel(RequestContext context, EventQueue eventQueue) throws JSONRPCError {/* 省略 */}

        private String extractTextFromMessage(Message message) {
            StringBuilder textBuilder = new StringBuilder();
            if (message.getParts() != null) {
                for (Part part : message.getParts()) {
                    if (part instanceof TextPart textPart) {
                        textBuilder.append(textPart.getText());
                    }
                }
            }
            return textBuilder.toString();
        }
    }
}

動作確認

A2Aのサーバ側の実装が正しく動作するかどうかを確認するため、A2A公式GitHubから提供されている a2a-inspector を利用する。

現時点での動作確認手順は以下だが、動作確認手順のREADMEが頻繁に更新されるので、手元のバージョンのREADMEをよく読んだほうが良い。

$ git clone https://github.com/a2aproject/a2a-inspector.git
$ cd a2a-inspector
$ uv sync
$ cd frontend
$ npm install
$ bash ../scripts/run.sh

a2a-inspectorが起動したらブラウザで http://localhost:5001 にアクセスし、Enter Agent Card URL欄にhttp://localhost:8080を入力して「Connect」する。
AgentCardが取得できるはずなので、取得できたら「Chat」エリアでチャットできることを確認する。

A2Aクライアントの実装

A2Aのクライアント実装する前に、https://a2a-protocol.org/latest/topics/what-is-a2a/#a2a-request-lifecycle のライフサイクルを理解しておいたほうが良い。
ポイントをざっくり述べると、Agentからのレスポンスを非ストリーミングで受け取るsendMessage API形式か、ストリーミングで受け取るsendMessageStream API形式かで参考にするサンプルコードが異なる。

上記の実装ではA2Aサーバ側をストリーミングでレスポンスするように実装しているため、今回は後者のストリーミングで受け取るTaskUpdateEventを処理する実装が必要になる。

しかしながら、公式GitHubから提供されているhelloworldのクライアント実装は非ストリーミングなMessageEventの例のため、TaskUpdateEventでストリーミングによるA2Aレスポンスを受信する例を把握することができない。
こちらの公式サンプルではJavaのクライアントサンプルコードが存在せず、Pythonのサンプルコードを参考にするしかない。

試行錯誤した結果、以下で動作確認が取れたのでコードを載せておく。

@QuarkusTest // Quarkusサーバを起動してテスト実行するアノテーション
public class A2aClientTest {
    @TestHTTPResource
    URI baseUri; // テスト用に起動したQuarkusサーバのベースURI(http://localhost:8081)

    private static final String MESSAGE_TEXT = "こんにちは";

    @Test
    void testA2AClient() throws Exception {
        final String SERVER_URL = this.baseUri.toString();
        AgentCard finalAgentCard = null;
        AgentCard publicAgentCard = new A2ACardResolver(SERVER_URL).getAgentCard();
        System.out.println(publicAgentCard);
        finalAgentCard = publicAgentCard;

        if (publicAgentCard.supportsAuthenticatedExtendedCard()) {/* 省略 */}

        final StringBuilder messageBuilder = new StringBuilder();
        final CompletableFuture<String> messageResponse = new CompletableFuture<>();
        List<BiConsumer<ClientEvent, AgentCard>> consumers = new ArrayList<>(); // A2A非同期レスポンス受信用consumer
        consumers.add((event, agentCard) -> {   // 非同期受信結果を処理する
            switch (event) {
                case TaskUpdateEvent updateEvent -> {
                    if (updateEvent.getUpdateEvent() instanceof TaskArtifactUpdateEvent artifactEvent) {
                        System.out.println("TaskArtifactUpdateEvent: Agentの成果物が更新されました");
                        for (Part<?> part : artifactEvent.getArtifact().parts()) {
                            System.out.println(part.getKind()); //成果物の種類 TEXT/FILE/DATAのいずれか
                            if (part instanceof TextPart textPart) {
                                messageBuilder.append(textPart.getText()); // 非同期メッセージを組み合わせる
                            }
                        }
                    } else if (updateEvent.getUpdateEvent() instanceof TaskStatusUpdateEvent statusEvent) {
                        System.out.println("TaskStatusUpdateEvent: タスクの状態が変化しました");
                        System.out.println(statusEvent.getKind());
                        System.out.println(statusEvent.getStatus().state());
                        System.out.println(statusEvent.getStatus().timestamp());
                        if (statusEvent.getStatus().state() == TaskState.COMPLETED) {
                            messageResponse.complete(messageBuilder.toString()); // 受信完了のため、非同期受信したメッセージを書き込む
                        }
                    }
                }
                default -> System.out.println(event.getClass().getSimpleName());
            }
        });

        Consumer<Throwable> streamingErrorHandler = (error) -> {
            System.err.println("ストリーミングエラーが発生しました: " + error.getMessage());
            error.printStackTrace();
            messageResponse.completeExceptionally(error);
        };

        Client client = Client
                .builder(finalAgentCard)
                .addConsumers(consumers)
                .streamingErrorHandler(streamingErrorHandler)
                .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig())
                .build();

        Message message = A2A.toUserMessage(MESSAGE_TEXT); // A2A用送信メッセージに変換
        System.out.println("メッセージ送信: " + MESSAGE_TEXT);
        client.sendMessage(message); // A2Aサーバに送信
        System.out.println("メッセージ送信完了。レスポンスは設定されたコンシューマーで処理されます。");

        try {
            String responseText = messageResponse.get(10, TimeUnit.SECONDS); // 非同期受信(ストリーミング)結果完了までスレッドブロックして完了したら受け取っているStringデータ(Agentの返信テキスト)を返す
            System.out.println("レスポンス: " + responseText);
        } catch (Exception e) {
            Assertions.fail("レスポンスの取得中に例外が発生しました", e);
        }
    }
}

Quarkusテストで動作確認

gradle testを実行するとA2Aサーバ(Quarkus)が起動、A2Aクライアント(jUnit)からA2A通信する動作確認を行う。なお、テスト用に起動するQuarkusはポート番号が8081となるため、AgentCardのurlも変更しておく必要がある。

app/src/main/java/org/example/SampleAgentCardProducer.java
    public AgentCard agentCard() {
        return new AgentCard.Builder()
                .name("my-agent")
                .description("役立つエージェント")
                .url("http://localhost:8081") // テスト用にポートを変更
$ export GOOGLE_API_KEY="取得したAPIキー"
$ ./gradlew clean test -i

さいごに

今回の検証で Java(Quarkus) + GoogleADK + A2A の初歩的なテキストベースの通信ができるようになった。
自分はPythonを使った経験が少なく苦手なので、Pythonの例が多い生成AI界隈の開発を理解するのが大変なのだが、今回Javaで検証したことでA2Aのコアとなる処理をなんとなく把握できたように思う。

まだまだA2A自体も発展途上だし、今後のアップデートで破壊的な更新も加わるだろうと思っているので、v1.0.0が登場するまでは様子見しても良いのでは、と思っていたりもする。
A2A以外にもACP(Agent Communication Protocol)のようなライバルプロトコルもあるので、今後のデファクトがどうなるか次第でもある。
デファクトといえば、JavaではまだQuarkusよりSpringBootのほうがデファクトだと思っているので、SpringAIの動向も気になる。

いずれにせよ、生成AI関係の開発案件は、開発技術よりプロダクトビジョン(企画や戦略の策定)に注力するほうが重要な気がしている。(AgentとAgentが通信しないと解決できない課題とは? 1サービス内にサブAgentを持つ方法との違いは? Agent間通信を想定するAgentってどんなことができるAgent?など)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?