How Java and Python Agents Collaborate through a Standard Protocol
Enterprise AI rarely exists in a single language, making building cross-language AI agents essential for modern architectures. Java typically manages customer-facing APIs, identity, policy, and long-running business services. Python handles model experimentation, data tooling, and specialist agent workflows. Connecting the two with bespoke REST endpoints creates an unstandardized, private protocol that both teams must continuously maintain.
The Agent2Agent (A2A) protocol offers a more durable boundary. This article builds a Spring AI coordinator in Java that delegates supplier research to a Google Agent Development Kit (ADK) specialist in Python. The frameworks stay independent; the network contract becomes the integration point.
The Business Value
A standardized agent boundary transforms each new agent integration from a custom software project into a straightforward configuration task. The primary business benefits emerge in four key areas:
- Faster delivery. One discovery and messaging contract replaces a private API for every agent pairing.
- Independent releases. The Python team changes prompts, models, or tools without a Java release, as long as the published capability stays compatible.
- Existing investment reused. Java keeps identity, audit, and policy; Python keeps its data tooling.
- Less lock-in. A2A now sits under the Linux Foundation’s Agentic AI Foundation, with support from major clouds and SaaS platforms.
Use Cases
This integration pattern is suitable wherever a governed Java front door must invoke specialist reasoning owned by a separate team. When building cross-language AI agents, the primary scenario is detailed in the first row of the table below, while the additional use cases follow an identical structure.
| Domain | Java coordinator owns | Python specialist owns |
|---|---|---|
| Procurement (this example) | Supplier assessment API, approval policy | Supplier risk research and signal scoring |
| Banking | Customer channels, KYC records, audit trail | Fraud and transaction-pattern analysis |
| Insurance | Claims intake and adjuster workflow | Document extraction and damage assessment |
| Healthcare operations | Patient scheduling and consent checks | Clinical-document summarization for staff review |
| Customer support | Ticketing, entitlements, SLA rules | Knowledge-base retrieval and resolution drafting |
In each case, the coordinator keeps the final decision and the human-review policy. The specialist supplies evidence, not verdicts.
Architecture
When a user requests a supplier assessment through the Java API, the Spring AI coordinator determines that external research is required and invokes the researchSupplier tool. The adapter reads the specialist’s Agent Card, dispatches an A2A message, and returns the result. Finally, the coordinator separates the remote findings from its own internal inference.
Only two steps involve model inference: the coordinator selecting the appropriate tool, and the specialist determining how to execute its domain tool. Discovery, endpoint selection, serialization, timeouts, and error handling remain in deterministic code, preventing the model from invoking arbitrary endpoints.
Why Building Cross-Language AI Agents Needs A2A Over Custom REST
While a standard REST endpoint eliminates runtime coupling, development teams must still define custom semantics for discovery, task states, streaming, artifacts, and error handling. The A2A protocol standardizes these core agent concepts while maintaining abstraction over remote implementations. Consequently, Spring AI only requires knowledge of the specialist’s capabilities and endpoint location, without needing visibility into its prompts, models, or state management.
Versions Used
The example was verified against the versions below as of early October 2026. Pin them in your build files; protocol SDKs and framework integrations move quickly.
| Component | Version | Source |
|---|---|---|
| Java | 21 | Required baseline for Spring AI 2.x |
| Spring Boot | 4.1.1 | Current stable, released 20 Aug 2026 |
| Spring AI | 2.0.1 | Released 21 Aug 2026; 2.0.x designed for Spring Boot 4.0 and 4.1 |
| A2A specification | 1.0 | Latest released version; stable since March 2026 |
| Python | 3.12 | Any version supported by ADK |
| Google ADK | 2.9.2 | PyPI |
| A2A Python SDK | 1.1.4 | Implements A2A 1.0 with a 0.3 compatibility mode |
You need an API key for the model behind Spring AI and a Google API key (or Google Cloud credentials) for Gemini. The supplier data is a local fixture, so no third-party research API is required.
Build the Python Specialist
The following implementation code is fully executable as presented. Install Google ADK alongside its A2A extension and pin the A2A SDK to ensure explicit transport versioning:
mkdir supplier-agent && cd supplier-agent
uv init --package --python 3.12
uv add "google-adk[a2a]==2.9.2" "a2a-sdk[http-server]==1.1.4" uvicornThe underlying tool returns a deterministic fixture to allow testing of the bridge without external dependencies. The system prompt explicitly identifies this data, ensuring the model does not present demonstration fixtures as live operational research.
# src/supplier_agent/agent.py
from google.adk.agents import Agent
from google.adk.a2a.utils.agent_to_a2a import to_a2a
def lookup_supplier_signals(company: str, country: str) -> dict:
"""Return demonstration-only supplier signals."""
return {
"company": company, "country": country,
"source": "demonstration fixture",
"signals": [
{"category": "operations", "finding": "Single regional warehouse"},
{"category": "finance", "finding": "Payment terms not verified"},
{"category": "compliance", "finding": "Human review required"},
],
}
root_agent = Agent(
name="supplier_research_agent",
model="gemini-flash-latest",
description="Researches supplier operating and compliance risk",
instruction="""You are a supplier research specialist.
Use lookup_supplier_signals for every assessment.
State that demonstration fixtures are not live evidence.
Return findings, assumptions, and follow-up checks.
Do not make a procurement decision.""",
tools=[lookup_supplier_signals],
)
# ADK builds the A2A server, task store, and Agent Card.
a2a_app = to_a2a(root_agent, port=8001)
Google ADK’s to_a2a() function encapsulates an existing agent as an A2A service and automatically generates its Agent Card. Because ADK 2.9.2 designates its A2A integration as experimental, explicit dependency versioning should be maintained. Start the service and verify discovery as follows:
export GOOGLE_API_KEY="replace-me"
uv run uvicorn supplier_agent.agent:a2a_app --host 0.0.0.0 --port 8001
# In another terminal
curl -H "A2A-Version: 1.0" http://localhost:8001/.well-known/agent-card.jsonThe card lists the agent’s skills, input and output modes, and its endpoints. Under A2A 1.0, endpoints appear in a supportedInterfaces array, each declaring its own protocol binding and version.
Build the Spring AI Coordinator
The Java implementation below is fully executable (with package declarations and imports omitted from the secondary snippet for brevity). To maintain clear architecture boundaries, the table below outlines framework-provided elements versus custom application code.
| Name | Comes from |
|---|---|
| ChatClient, @Tool, @ToolParam, defaultTools, ToolCallingAdvisor | Spring AI 2.0.1 (tool calling) |
| RestClient, JdkClientHttpRequestFactory, ParameterizedTypeReference | Spring Framework 7 (via Spring Boot 4.1.1) |
| A2aResearchTool, resolveEndpoint, the JSON-RPC request map | Application code in this article |
| SendMessage, ROLE_USER, supportedInterfaces, A2A-Version | Wire-format names from the A2A 1.0 specification |
The example does not depend on any A2A support inside Spring AI. It uses plain HTTP so every protocol detail is visible.
pom.xml (dependencies)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>2.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
</dependencies>application.yml
spring.ai.openai:
api-key: ${OPENAI_API_KEY}
chat.model: ${OPENAI_MODEL}
agents.supplier:
card-url: http://localhost:8001/.well-known/agent-card.json
allowed-hosts: localhostThe OpenAI starter is only an example. Any Spring AI chat model that supports tools can drive the adapter.
The A2A Adapter Tool for Building Cross-Language AI Agents
Annotation-based tools in Spring AI are synchronous, so the adapter blocks at the tool boundary with fixed timeouts. Every request carries the A2A-Version: 1.0 header, which the A2A 1.0 specification (section 3.6.1) requires clients to send; a server that receives no header must assume version 0.3.
package com.example.coordinator;
import java.net.URI;
import java.net.http.HttpClient;
import java.time.Duration;
import java.util.*;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
@Component
public class A2aResearchTool {
private static final ParameterizedTypeReference<Map<String, Object>> JSON =
new ParameterizedTypeReference<>() {};
private final RestClient http;
private final URI cardUri;
private final Set allowedHosts;
public A2aResearchTool(
@Value("${agents.supplier.card-url}") URI cardUri,
@Value("${agents.supplier.allowed-hosts}") Set allowedHosts)
{
var factory = new JdkClientHttpRequestFactory(
HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build());
factory.setReadTimeout(Duration.ofSeconds(45));
this.http = RestClient.builder()
.requestFactory(factory)
.defaultHeader("A2A-Version", "1.0")
.build();
this.cardUri = cardUri;
this.allowedHosts = allowedHosts;
}
@Tool(description = "Research a supplier through the remote specialist agent")
public String researchSupplier(
@ToolParam(description = "Legal or trading company name") String company,
@ToolParam(description = "Country of operation") String country) {
Map<String, Object> request = Map.of(
"jsonrpc", "2.0",
"id", UUID.randomUUID().toString(),
"method", "SendMessage",
"params", Map.of("message", Map.of(
"messageId", UUID.randomUUID().toString(),
"role", "ROLE_USER",
"parts", List.of(Map.of("text",
"Assess supplier " + company + " in " + country)))));
Map<String, Object> response = http.post().uri(resolveEndpoint())
.contentType(MediaType.APPLICATION_JSON)
.body(request).retrieve().body(JSON);
if (response == null || response.containsKey("error")) {
throw new IllegalStateException("A2A call failed: " + response);
}
return String.valueOf(response.get("result"));
}
/** Reads the Agent Card and returns a JSON-RPC endpoint on an allowed host. */
@SuppressWarnings("unchecked")
private URI resolveEndpoint() {
Map<String, Object> card = http.get().uri(cardUri).retrieve().body(JSON);
var interfaces = (List<Map<String, Object>>)
card.getOrDefault("supportedInterfaces", List.of());
return interfaces.stream()
.filter(i -> "JSONRPC".equals(i.get("protocolBinding")))
.map(i -> URI.create((String) i.get("url")))
.filter(u -> allowedHosts.contains(u.getHost()))
.findFirst()
.orElseThrow(() ->new IllegalStateException("No allowed JSON-RPC endpoint"));
}
}The host allowlist provides necessary security control: the model determines business parameters while deterministic code enforces target endpoints. For production implementations, this custom client should be replaced with the official A2A Java SDK, which provides strongly typed domain models, Agent Card caching, and comprehensive task state management.
Register the coordinator
@Configuration
class ChatConfiguration {
@Bean
ChatClient coordinator(ChatClient.Builder builder, A2aResearchTool researchTool) {
return builder.defaultSystem("""
You assess supplier risk for a human reviewer.
Use researchSupplier before writing an assessment.
Keep remote findings separate from your own inference.
Never approve or reject a supplier.""")
.defaultTools(researchTool)
.build();
}
}
@RestController
class AssessmentController {
private final ChatClient coordinator;
AssessmentController(ChatClient coordinator) {
this.coordinator = coordinator;
}
record AssessmentRequest(String company, String country) {}
@PostMapping("/api/assessments")
Map<String, String> assess(@RequestBody AssessmentRequest req) {
String answer = coordinator.prompt()
.user("Assess " + req.company() + " in " + req.country())
.call().content();
return Map.of("assessment", answer);
}
}Spring AI’s auto-registered ToolCallingAdvisor runs the model-and-tool loop until the model returns a final answer. Spring AI 2.0.1 also adds a configurable cap on tool calls per request, worth setting in production.
Run and Debug
With the Python service running, start Spring and send one request:
export OPENAI_API_KEY="replace-me"
export OPENAI_MODEL="a-tool-capable-model"
./mvnw spring-boot:run
curl -X POST http://localhost:8080/api/assessments \
-H "Content-Type: application/json" \
-d '{"company":"Northwind Components","country":"India"}'Although response phrasing varies by model, the final output should cite the demonstration data source, summarize the three fixture signals, and request human verification. If the coordinator issues a final procurement decision, refine the system instructions and implement automated policy checks.
During troubleshooting, invoke the Python agent directly to isolate transport and specialist errors from Spring AI tool selection issues. Target the endpoint URL specified in the Agent Card using the updated A2A 1.0 specification naming, where message/send has been renamed to SendMessage and the redundant kind field has been removed:
curl -X POST <JSONRPC-url-from-agent-card> \
-H "Content-Type: application/json" \
-H "A2A-Version: 1.0" \
-d '{"jsonrpc":"2.0","id":"debug-1","method":"SendMessage",
"params":{"message":{"messageId":"msg-1","role":"ROLE_USER",
"parts":[{"text":"Assess Northwind in India"}]}}}'The open-source A2A Inspector and Technology Compatibility Kit (TCK) offer dedicated tools for interactive protocol inspection and compliance validation during SDK upgrades.
Prepare for Production
A successful demonstration confirms baseline connectivity but does not guarantee operational resilience. Before introducing the adapter into production workflows, address and validate the following operational requirements:
| Concern | Practice | Test that proves it |
|---|---|---|
| Trust | TLS everywhere; read security schemes from the Agent Card but enforce a local allowlist; keep credentials out of prompts and message parts | A card with an unlisted host is rejected |
| Timeouts and retries | Separate deadlines for discovery and execution; retry discovery only; treat SendMessage as non-idempotent after a timeout | A retried message creates no duplicate business action |
| Long-running work | Store the task ID behind a durable job; collect results with GetTask, SubscribeToTask, or push notifications | A TASK_STATE_WORKING result is persisted and resumed |
| Interrupted tasks | Surface TASK_STATE_INPUT_REQUIRED questions to the user | No value is invented to fill the gap |
| Observability | Propagate trace context across Java, A2A, Python, and model calls; log agent name, task ID, latency, terminal state | One trace spans both services |
| Untrusted output | Treat remote text as evidence, never as policy; validate structured fields before storing them | Injected instructions in a reply are ignored |
| Versioning | Send A2A-Version, pin SDKs, run a cross-language contract suite on every upgrade | An unsupported version returns a controlled error |
When to Choose A2A
Adopt A2A when organisational independence is as critical as technical decoupling: distinct engineering teams manage each agent, specialists scale and deploy independently, and internal tools or prompts remain encapsulated. A2A complements the Model Context Protocol (MCP), which standardises agent-to-tool connections rather than inter-agent communication.
Skip it when the operation is deterministic with a stable API, or when both parts share a release cycle. A single lookup is a tool, not an agent.
Future Trends
Agent interoperability is moving from experiment to infrastructure. Five shifts are worth planning for:
- Neutral governance. A2A joined the Agentic AI Foundation in August 2026, alongside MCP, reducing single-vendor risk.
- Platform-native agents. The same announcement reports native support in Google Cloud, AWS Bedrock AgentCore, and Azure AI Foundry, and adoption by ServiceNow, Salesforce, Atlassian, and SAP.
- Verifiable identity. A2A 1.0 defines Agent Card signing; signed cards will likely replace hand-kept allowlists as the first trust check.
- Typed SDKs and tooling. Official SDKs cover six languages, and an A2A CLI shipped in October 2026. Hand-written adapters like this one become teaching tools.
- Larger tool surfaces. Sending every tool definition on every request stops scaling as coordinators gain remote agents. Spring AI 2.0 already ships a tool-search advisor that exposes tools on demand.
Conclusion
Java and Python agents can effectively collaborate without sharing a common runtime or framework ecosystem. Spring AI manages orchestration, policy enforcement, and client delivery; Google ADK governs specialist reasoning and domain tools; and A2A provides the standardized protocol for discovery, messaging, task lifecycle management, and artifact transfer.
The adapter pattern demonstrated in this article establishes a baseline integration in approximately 60 lines of code. Enterprise deployments should extend this foundation with the official A2A Java SDK, mutual authentication, persistent task state, distributed tracing, strict schema validation, and contract testing suites. With these controls established, independent engineering teams can safely evolve cross-language agent systems against a stable specification.
This article was supported by AI-based research and writing, with Claude 5 assisting in the creation of text and images.
Author
Saleen Theeyons is a Senior Software Engineer at CapeStart with 11+ years of experience in software engineering, specializing in Java, Spring Boot, microservices, and backend development. Passionate about Artificial Intelligence (AI) and Generative AI, he explores emerging technologies and how intelligent tools can enhance software development practices, improve productivity, and drive innovation. He enjoys experimenting with modern technologies and sharing practical insights through technical articles, focusing on the evolving landscape of software engineering and the growing impact of AI on application development.
FAQ
What is the Agent2Agent (A2A) protocol?
An open standard for communication between independent AI agents. It defines how agents publish capabilities, exchange messages and artifacts, and track tasks without exposing prompts, tools, or memory.
Does this example need A2A support inside Spring AI?
No. The adapter uses plain Spring RestClient calls so every protocol detail is visible. For production, use the official A2A Java SDK.
Why send the A2A-Version header?
The A2A 1.0 specification requires clients to send it. A server that receives no header treats the request as version 0.3, with older method names and field shapes.
How is A2A different from MCP?
A2A connects agents to other agents; MCP connects an agent to its tools and data. They are complementary: a specialist can use MCP internally while exposing one A2A capability.
How should long-running tasks be handled?
Do not hold a public HTTP request open. Store the task ID behind a durable job, then collect the result with GetTask, SubscribeToTask, or push notifications.