The Agent2Agent (A2A) protocol lets one agent call another over HTTP without knowing what framework, model or language the other uses. The caller reads a public description, the agent card, and then exchanges messages and tasks using a fixed JSON-RPC (or REST or gRPC) contract. For Java teams building on Google's Agent Development Kit, the google-adk-a2a module is the bridge: it turns any ADK agent into an A2A server, and turns any remote A2A agent into something an ADK orchestrator can delegate to as if it were a local sub-agent.
This page covers both directions in Java specifically, from the dependency coordinates to the classes, the request flow, and the operational traps. The protocol itself is covered in the A2A agent card guide and the Python side of ADK in ADK and A2A interop. Class and property names here were checked against the google/adk-java main branch and its samples on 2026-10-02; the module is young, so confirm them against the version you pin.
Why put a protocol between agents
Inside one ADK application, delegation is cheap: an LlmAgent lists sub-agents, and the model transfers control to one of them in the same process, sharing the session. That stops working when the specialist is owned by another team, deployed on its own schedule, written in Python, or scaled independently. You could expose it as an ordinary REST tool, but then every caller hand-writes a client and loses the conversational shape: multi-turn context, long-running tasks, streamed progress and artifacts.
A2A standardises exactly that shape. The server publishes a card describing its skills, input and output modes, capabilities such as streaming, and security schemes. The client sends a Message made of parts (text, structured data, files) and gets back either a message or a Task that moves through states such as submitted, working, input-required, completed, failed and canceled, with artifacts attached. A contextId ties related turns together. The caller never sees the callee's prompts, tools or session store.
The pieces in the Java stack
There are three layers, and keeping them apart explains most of the configuration.
| Layer | What it provides | Key types |
|---|---|---|
| a2a-java SDK | Protocol types, client, server request handling, transports | io.a2a.spec.AgentCard, io.a2a.client.Client, io.a2a.server.agentexecution.AgentExecutor |
| google-adk-a2a | Mapping between ADK events and A2A messages, tasks and artifacts | com.google.adk.a2a.executor.AgentExecutor, com.google.adk.a2a.agent.RemoteA2AAgent, converters |
| Server runtime | HTTP endpoints, dependency injection, config | Quarkus reference JSON-RPC server in the samples |
On the server, ADK's AgentExecutor implements the SDK's AgentExecutor interface: the SDK handles HTTP and JSON-RPC, then calls execute(RequestContext, EventQueue); ADK converts the inbound message to ADK content, runs your agent through an ADK Runner, and publishes task status and artifact events back onto the queue. On the client, RemoteA2AAgent extends ADK's BaseAgent, so an orchestrator treats it like any other sub-agent while it actually calls the SDK client.
Exposing an ADK agent
Add the ADK A2A module plus a server transport. The samples use the a2a-java reference JSON-RPC server on Quarkus:
<dependency>
<groupId>com.google.adk</groupId>
<artifactId>google-adk-a2a</artifactId>
<version>${google-adk.version}</version>
</dependency>
<!-- server side only: the a2a-java reference JSON-RPC transport (Quarkus) -->
<dependency>
<groupId>io.github.a2asdk</groupId>
<artifactId>a2a-java-sdk-reference-jsonrpc</artifactId>
<version>${a2a.sdk.version}</version> <!-- match what google-adk-a2a pins -->
</dependency>Then produce two beans: the executor that wraps your agent, and the public agent card. This is the shape of the contrib sample in adk-java, trimmed:
@ApplicationScoped
public class AgentExecutorProducer {
@ConfigProperty(name = "my.adk.app.name", defaultValue = "prime-service")
String appName;
@Produces
public io.a2a.server.agentexecution.AgentExecutor agentExecutor() {
return new com.google.adk.a2a.executor.AgentExecutor.Builder()
.agent(PrimeAgent.ROOT_AGENT) // any ADK BaseAgent
.appName(appName)
.sessionService(new InMemorySessionService()) // swap for a durable store
.artifactService(new InMemoryArtifactService())
.agentExecutorConfig(AgentExecutorConfig.builder().build())
.build();
}
}
@ApplicationScoped
public class AgentCardProducer {
@Produces @PublicAgentCard
public AgentCard agentCard() throws IOException {
try (InputStream is = getClass().getResourceAsStream("/agent/agent.json")) {
return Utils.OBJECT_MAPPER.readValue(is.readAllBytes(), AgentCard.class);
}
}
}The executor builder also accepts memoryService and plugins, so callbacks, logging and policy plugins you already use locally carry over. AgentExecutorConfig controls the run: its default RunConfig disables model streaming and caps a run at 20 model calls, and its OutputMode is ARTIFACT_PER_RUN (one artifact holding the final result) unless you choose ARTIFACT_PER_EVENT. It also takes before-execute, after-execute and after-event callbacks, the natural hook for audit logging and for redacting what leaves your service.
The card is served at /.well-known/agent-card.json. It is a contract, so write it with care:
{
"name": "check_prime_agent",
"description": "Checks whether numbers are prime.",
"url": "https://primes.internal.example.com",
"version": "1.0.0",
"preferredTransport": "JSONRPC",
"capabilities": { "streaming": true },
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["application/json"],
"skills": [{ "id": "prime_checking", "name": "Prime checking",
"description": "Primality of one number or a list",
"tags": ["math"] }]
}The description and skills are what a calling model reads when deciding whether to delegate, so they deserve the same attention as a tool description. The url must be the externally reachable address, not localhost, or every client will resolve the card and then fail to connect.
Consuming a remote agent
On the calling side you resolve the card, build an SDK client for the transport the card prefers, and wrap it in a RemoteA2AAgent:
static BaseAgent remotePrimeAgent(String baseUrl) throws Exception {
AgentCard card = new A2ACardResolver(
new JdkA2AHttpClient(), baseUrl, baseUrl + "/.well-known/agent-card.json")
.getAgentCard();
Client client = Client.builder(card)
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig())
.clientConfig(new ClientConfig.Builder()
.setStreaming(card.capabilities().streaming())
.build())
.build();
return RemoteA2AAgent.builder()
.name(card.name())
.agentCard(card)
.a2aClient(client)
.streaming(true) // effective only if the card also says streaming
.build();
}
LlmAgent root = LlmAgent.builder()
.name("root_agent")
.model("gemini-2.5-flash")
.instruction("Delegate primality questions to check_prime_agent.")
.subAgents(ImmutableList.of(rollAgent, remotePrimeAgent(primesUrl)))
.build();Two details matter. First, streaming needs three conditions to hold: the card advertises capabilities.streaming, the SDK client config calls setStreaming(true), and the RemoteA2AAgent builder sets streaming(true). If any one of them is false, you get a single response at the end. Second, RemoteA2AAgent builds its outgoing message from the session: if the latest event is the user answering a function call that came from the remote side (the remote agent asked for input), it sends that answer with the original task and context IDs so the remote task resumes; otherwise it assembles a fresh message from the invocation context. Responses come back as ADK events, so the orchestrator's session, callbacks and callback chain see remote turns like local ones.
A worked request, end to end
A user asks the orchestrator: roll a 20-sided die and tell me if the result is prime.
- The root
LlmAgentroutes the roll to its local roll agent, which calls a function tool and records, say, 17. - The model then transfers to
check_prime_agent, theRemoteA2AAgent. It builds a user-role A2A message whose parts carry the relevant text, generates a message ID, and callsclient.sendMessage. - The remote reference server receives the JSON-RPC call, creates or resumes a task, and invokes ADK's
AgentExecutor.execute. The executor marks the task submitted, then working, and runs the prime agent through its ownRunnerwith its own session. - The prime agent calls its tool and answers that 17 is prime. The executor converts the final ADK event into an artifact and a completed status event and puts them on the queue.
- Back on the caller, the SDK client delivers those events;
RemoteA2AAgentconverts them into ADK events, and the root agent recaps: you rolled 17, which is prime.
Each side kept its own session. That is a feature, since the specialist never sees unrelated history, but it means anything the specialist needs must travel in the message. If answers seem to ignore earlier context, inspect the outgoing message before blaming the remote model.
Timeouts, streaming and long tasks
The reference server's blocking path waits for the agent to finish. The adk-java sample sets a2a.blocking.agent.timeout.seconds=30 and a2a.blocking.consumption.timeout.seconds=5 in application.properties, which are the defaults it documents. An agent that calls a slow tool or several models can exceed 30 seconds easily; raise the limit deliberately or, better, make long work streaming so the client receives working-state updates instead of one late reply.
On the client side, set connect and read timeouts on the HTTP client and an overall deadline on the delegation, and decide what the orchestrator should say when the remote agent is down. Wrapping the remote call in the same circuit-breaker logic you use for any dependency, as in the ADK Java circuit breaker guide, keeps a failing specialist from stalling every conversation.
Failure modes
Most problems with this integration are configuration, not code.
| Symptom | Likely cause | Fix |
|---|---|---|
| NoSuchMethodError or missing classes at startup | Mixed a2a-java SDK versions or groupIds on the classpath | Align every a2a-java artifact to the version google-adk-a2a pins; check mvn dependency:tree |
| Card resolves, then connection refused | Card url still says localhost or an internal port | Publish the external URL; generate the card per environment |
| Replies arrive all at once | Streaming off in the card, the client config or the RemoteA2AAgent builder | Enable it in all three |
| Calls fail after about 30 seconds | Blocking agent timeout | Raise a2a.blocking.agent.timeout.seconds or stream |
| Remote agent forgets earlier turns | Each side has its own session; context not in the message | Put needed facts in the parts; reuse contextId for follow-ups |
| Run stops early with no answer | Default cap of 20 model calls | Set a RunConfig with a higher limit if the agent legitimately needs it |
The first row deserves emphasis. The google-adk-a2a module on main pins the a2a-java SDK at 0.3.2.Final under the io.github.a2asdk groupId, which speaks A2A protocol 0.3, while the upstream a2a-java README now publishes under org.a2aproject.sdk. Two groupIds mean Maven will happily put both on the classpath. Pick the SDK line your ADK release was built against and enforce it with a dependency-management block or an enforcer rule.
Security and operations
The card is public by design, so treat its contents as published documentation and declare how to authenticate in its security schemes rather than leaving the endpoint open; A2A authentication covers the patterns. Terminate TLS in front of the Quarkus service, authenticate callers with OAuth client credentials or mutual TLS between services, and authorise per skill. Remember that the message parts you receive are untrusted input to your model, exactly like user text, so the same prompt-injection defences apply.
Replace the in-memory session and artifact services before production: with more than one replica, a follow-up turn may land on a different pod. Propagate trace context and log task IDs and context IDs on both sides so a single delegation can be followed across services; ADK Java observability shows the instrumentation. Version the card, and when changing a skill's contract, add a new skill rather than silently changing an existing one.
What to do next
- Run the adk-java
a2a_serversample, fetch its card with curl, and read every field. - Point the
a2a_basicclient sample at it, then turn streaming on in all three places and watch the events change. - Wrap one of your own ADK agents with
AgentExecutor, publish a card with an external URL, and call it from an orchestrator throughRemoteA2AAgent. - Run
mvn dependency:treeand confirm there is exactly one a2a-java SDK line on the classpath. - Set explicit timeouts on both sides, add a circuit breaker on the client, and replace in-memory sessions on the server.
- Add authentication to the card and endpoint, and log task and context IDs on every hop.