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.

Advertisement

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.

LayerWhat it providesKey types
a2a-java SDKProtocol types, client, server request handling, transportsio.a2a.spec.AgentCard, io.a2a.client.Client, io.a2a.server.agentexecution.AgentExecutor
google-adk-a2aMapping between ADK events and A2A messages, tasks and artifactscom.google.adk.a2a.executor.AgentExecutor, com.google.adk.a2a.agent.RemoteA2AAgent, converters
Server runtimeHTTP endpoints, dependency injection, configQuarkus 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.

ADK Java on both ends of A2A: RemoteA2AAgent as a client, AgentExecutor behind a JSON-RPC serverroot_agent (LlmAgent)orchestrator serviceRemoteA2AAgentsub-agent proxytransfera2a ClientJSONRPCTransportHTTPmessage/send or streamAgent card/.well-known/agent-card.jsonresolved at startupReference JSON-RPC serverQuarkus, a2a-java SDKAgentExecutor (ADK)Runner, sessions, artifactsprime_agent (LlmAgent)specialist serviceWhat crosses the wireA2A Message with text/data parts, taskId and contextId; replies as task status and artifact eventsNeither side sees the other's model, tools, prompts or session store: only the card and the protocol.
An orchestrator transfers to RemoteA2AAgent, which sends an A2A message through the SDK client; the remote reference server hands it to ADK's AgentExecutor, which runs the specialist agent and streams task events back.

Advertisement

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.

  1. The root LlmAgent routes the roll to its local roll agent, which calls a function tool and records, say, 17.
  2. The model then transfers to check_prime_agent, the RemoteA2AAgent. It builds a user-role A2A message whose parts carry the relevant text, generates a message ID, and calls client.sendMessage.
  3. 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 own Runner with its own session.
  4. 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.
  5. Back on the caller, the SDK client delivers those events; RemoteA2AAgent converts 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.

SymptomLikely causeFix
NoSuchMethodError or missing classes at startupMixed a2a-java SDK versions or groupIds on the classpathAlign every a2a-java artifact to the version google-adk-a2a pins; check mvn dependency:tree
Card resolves, then connection refusedCard url still says localhost or an internal portPublish the external URL; generate the card per environment
Replies arrive all at onceStreaming off in the card, the client config or the RemoteA2AAgent builderEnable it in all three
Calls fail after about 30 secondsBlocking agent timeoutRaise a2a.blocking.agent.timeout.seconds or stream
Remote agent forgets earlier turnsEach side has its own session; context not in the messagePut needed facts in the parts; reuse contextId for follow-ups
Run stops early with no answerDefault cap of 20 model callsSet 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

  1. Run the adk-java a2a_server sample, fetch its card with curl, and read every field.
  2. Point the a2a_basic client sample at it, then turn streaming on in all three places and watch the events change.
  3. Wrap one of your own ADK agents with AgentExecutor, publish a card with an external URL, and call it from an orchestrator through RemoteA2AAgent.
  4. Run mvn dependency:tree and confirm there is exactly one a2a-java SDK line on the classpath.
  5. Set explicit timeouts on both sides, add a circuit breaker on the client, and replace in-memory sessions on the server.
  6. Add authentication to the card and endpoint, and log task and context IDs on every hop.
Key takeaway: ADK Java's A2A module puts the protocol on both ends of a delegation: AgentExecutor turns any ADK agent into an A2A server behind the a2a-java reference transport, and RemoteA2AAgent lets an orchestrator delegate to any A2A agent as if it were a local sub-agent. The code is short; the work is in the contract and the operations: a careful card with a real URL, streaming enabled everywhere it must be, deliberate timeouts, one SDK version on the classpath, durable sessions, authentication and traceable task IDs.