> parts) {
+ final StringBuilder textBuilder = new StringBuilder();
+ if (parts != null) {
+ for (final Part> part : parts) {
+ if (part instanceof TextPart textPart) {
+ textBuilder.append(textPart.getText());
+ }
+ }
+ }
+ return textBuilder.toString();
+ }
+
+ private static void printUsageAndExit() {
+ System.out.println("Usage: TestClient [OPTIONS]");
+ System.out.println();
+ System.out.println("Options:");
+ System.out.println(" --server-url URL "
+ + "The URL of the A2A server agent (default: "
+ + DEFAULT_SERVER_URL + ")");
+ System.out.println(" --message TEXT "
+ + "The message to send to the agent "
+ + "(default: \"" + MESSAGE_TEXT + "\")");
+ System.out.println(" --help, -h "
+ + "Show this help message and exit");
+ System.out.println();
+ System.out.println("Examples:");
+ System.out.println(" TestClient --server-url http://localhost:11001");
+ System.out.println(" TestClient --message "
+ + "\"Can you roll a 12-sided die?\"");
+ System.out.println(" TestClient --server-url http://localhost:11001 "
+ + "--message \"Is 17 prime?\"");
+ System.exit(0);
+ }
+}
diff --git a/samples/java/agents/dice_agent_multi_transport/client/src/main/java/com/samples/a2a/client/TestClientRunner.java b/samples/java/agents/dice_agent_multi_transport/client/src/main/java/com/samples/a2a/client/TestClientRunner.java
new file mode 100644
index 00000000..bb5a1d77
--- /dev/null
+++ b/samples/java/agents/dice_agent_multi_transport/client/src/main/java/com/samples/a2a/client/TestClientRunner.java
@@ -0,0 +1,43 @@
+///usr/bin/env jbang "$0" "$@" ; exit $?
+//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.0.Beta1
+//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.0.Beta1
+//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-grpc:0.3.0.Beta1
+//DEPS com.fasterxml.jackson.core:jackson-databind:2.15.2
+//DEPS io.grpc:grpc-netty-shaded:1.69.1
+//SOURCES TestClient.java
+
+/**
+ * JBang script to run the A2A TestClient example for the Dice Agent. This
+ * script automatically handles the dependencies and runs the client.
+ *
+ *
+ * Prerequisites: - JBang installed (see
+ * https://www.jbang.dev/documentation/guide/latest/installation.html) - A
+ * running Dice Agent server (see README.md for instructions on setting up the
+ * agent)
+ *
+ *
+ * Usage: $ jbang TestClientRunner.java
+ *
+ *
+ * Or with a custom server URL: $ jbang TestClientRunner.java
+ * --server-url=http://localhost:10000
+ *
+ *
+ * The script will communicate with the Dice Agent server and send the message
+ * "Can you roll a 5 sided die" to demonstrate the A2A protocol interaction.
+ */
+public final class TestClientRunner {
+
+ private TestClientRunner() {
+ // this avoids a lint issue
+ }
+
+ /**
+ * Client entry point.
+ * @param args can optionally contain the --server-url and --message to use
+ */
+ public static void main(final String[] args) {
+ com.samples.a2a.client.TestClient.main(args);
+ }
+}
diff --git a/samples/java/agents/dice_agent_multi_transport/client/src/main/java/com/samples/a2a/client/package-info.java b/samples/java/agents/dice_agent_multi_transport/client/src/main/java/com/samples/a2a/client/package-info.java
new file mode 100644
index 00000000..74a05d0c
--- /dev/null
+++ b/samples/java/agents/dice_agent_multi_transport/client/src/main/java/com/samples/a2a/client/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Dice Agent package.
+ */
+package com.samples.a2a.client;
diff --git a/samples/java/agents/dice_agent_multi_transport/pom.xml b/samples/java/agents/dice_agent_multi_transport/pom.xml
new file mode 100644
index 00000000..a9354c06
--- /dev/null
+++ b/samples/java/agents/dice_agent_multi_transport/pom.xml
@@ -0,0 +1,80 @@
+
+
+ 4.0.0
+
+ com.samples.a2a
+ dice-agent-multi-transport
+ 0.1.0
+ pom
+
+
+ server
+ client
+
+
+
+ 17
+ 17
+ UTF-8
+ 4.31.1
+ 0.3.0.Beta1
+ 4.1.0
+ 3.26.1
+ 1.0.0
+
+
+
+
+
+ io.quarkus
+ quarkus-bom
+ ${quarkus.platform.version}
+ pom
+ import
+
+
+
+ com.google.protobuf
+ protobuf-java
+ ${protobuf.version}
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ 17
+
+
+
+ io.quarkus
+ quarkus-maven-plugin
+ ${quarkus.platform.version}
+ true
+
+
+
+ build
+ generate-code
+ generate-code-tests
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+
+
+
+
diff --git a/samples/java/agents/dice_agent_multi_transport/server/.env.example b/samples/java/agents/dice_agent_multi_transport/server/.env.example
new file mode 100644
index 00000000..cb2fe891
--- /dev/null
+++ b/samples/java/agents/dice_agent_multi_transport/server/.env.example
@@ -0,0 +1 @@
+QUARKUS_LANGCHAIN4J_AI_GEMINI_API_KEY=your_api_key_here
diff --git a/samples/java/agents/dice_agent_multi_transport/server/pom.xml b/samples/java/agents/dice_agent_multi_transport/server/pom.xml
new file mode 100644
index 00000000..d21b1e22
--- /dev/null
+++ b/samples/java/agents/dice_agent_multi_transport/server/pom.xml
@@ -0,0 +1,56 @@
+
+
+ 4.0.0
+
+
+ com.samples.a2a
+ dice-agent-multi-transport
+ 0.1.0
+
+
+ dice-agent-server
+ Dice Agent Server
+ A2A Dice Agent Server Implementation
+
+
+
+ io.github.a2asdk
+ a2a-java-sdk-reference-grpc
+ ${io.a2a.sdk.version}
+
+
+ io.github.a2asdk
+ a2a-java-sdk-reference-jsonrpc
+ ${io.a2a.sdk.version}
+
+
+ io.quarkus
+ quarkus-rest-jackson
+
+
+ jakarta.enterprise
+ jakarta.enterprise.cdi-api
+ ${jakarta.enterprise.cdi-api.version}
+
+
+ io.quarkiverse.langchain4j
+ quarkus-langchain4j-ai-gemini
+ ${quarkus.langchain4j.version}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ io.quarkus
+ quarkus-maven-plugin
+
+
+
+
diff --git a/samples/java/agents/dice_agent_multi_transport/server/src/main/java/com/samples/a2a/DiceAgent.java b/samples/java/agents/dice_agent_multi_transport/server/src/main/java/com/samples/a2a/DiceAgent.java
new file mode 100644
index 00000000..fb8f060e
--- /dev/null
+++ b/samples/java/agents/dice_agent_multi_transport/server/src/main/java/com/samples/a2a/DiceAgent.java
@@ -0,0 +1,48 @@
+package com.samples.a2a;
+
+import dev.langchain4j.service.SystemMessage;
+import dev.langchain4j.service.UserMessage;
+import io.quarkiverse.langchain4j.RegisterAiService;
+import jakarta.enterprise.context.ApplicationScoped;
+
+/** Dice agent. */
+@RegisterAiService(tools = DiceTools.class)
+@ApplicationScoped
+public interface DiceAgent {
+
+ /**
+ * Rolls dice and provides information about the outcome of dice roles.
+ *
+ * @param question the users' question
+ * @return the answer
+ */
+ @SystemMessage(
+ """
+ You roll dice and answer questions about the outcome of the dice rolls.
+ You can roll dice of different sizes. The only things you do are roll
+ dice for the user and discuss the outcomes.
+ It is ok to discuss previous dice roles, and comment on the dice rolls.
+ When you are asked to roll a die, you must call the rollDice tool with
+ the number of sides.
+ Be sure to pass in an integer. Do not pass in a string.
+ You should never roll a die on your own.
+ When checking prime numbers, call the checkPrime tool
+ with a list of integers.
+ Be sure to pass in a list of integers. You should never pass in a
+ string.
+ You should not check prime numbers before calling the tool.
+ When you are asked to roll a die and check prime numbers,
+ you should always make the following two function calls:
+ 1. You should first call the rollDice tool to get a roll.
+ Wait for the function response before calling the checkPrime tool.
+ 2. After you get the function response from rollDice tool, you
+ should call the checkPrime tool with the rollDice result.
+ 2.1 If user asks you to check primes based on previous rolls,
+ make sure you include the previous rolls in the list.
+ 3. When you respond, you must include the rollDice result from step 1.
+ You should always perform the previous 3 steps when asking for a roll
+ and checking prime numbers.
+ You should not rely on the previous history on prime results.
+ """)
+ String rollAndAnswer(@UserMessage String question);
+}
diff --git a/samples/java/agents/dice_agent_multi_transport/server/src/main/java/com/samples/a2a/DiceAgentCardProducer.java b/samples/java/agents/dice_agent_multi_transport/server/src/main/java/com/samples/a2a/DiceAgentCardProducer.java
new file mode 100644
index 00000000..3ba7adba
--- /dev/null
+++ b/samples/java/agents/dice_agent_multi_transport/server/src/main/java/com/samples/a2a/DiceAgentCardProducer.java
@@ -0,0 +1,82 @@
+package com.samples.a2a;
+
+import io.a2a.server.PublicAgentCard;
+import io.a2a.spec.AgentCapabilities;
+import io.a2a.spec.AgentCard;
+import io.a2a.spec.AgentInterface;
+import io.a2a.spec.AgentSkill;
+import io.a2a.spec.TransportProtocol;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Produces;
+import jakarta.inject.Inject;
+import java.util.List;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+/**
+ * Producer for dice agent card configuration.
+ */
+@ApplicationScoped
+public final class DiceAgentCardProducer {
+
+ /** The HTTP port for the agent service. */
+ @Inject
+ @ConfigProperty(name = "quarkus.http.port")
+ private int httpPort;
+
+ /**
+ * Produces the agent card for the dice agent.
+ *
+ * @return the configured agent card
+ */
+ @Produces
+ @PublicAgentCard
+ public AgentCard agentCard() {
+ return new AgentCard.Builder()
+ .name("Dice Agent")
+ .description(
+ "Rolls an N-sided dice and answers questions about the "
+ + "outcome of the dice rolls. Can also answer questions "
+ + "about prime numbers.")
+ .preferredTransport(TransportProtocol.GRPC.asString())
+ .url("localhost:" + httpPort)
+ .version("1.0.0")
+ .documentationUrl("http://example.com/docs")
+ .capabilities(
+ new AgentCapabilities.Builder()
+ .streaming(true)
+ .pushNotifications(false)
+ .stateTransitionHistory(false)
+ .build())
+ .defaultInputModes(List.of("text"))
+ .defaultOutputModes(List.of("text"))
+ .skills(
+ List.of(
+ new AgentSkill.Builder()
+ .id("dice_roller")
+ .name("Roll dice")
+ .description("Rolls dice and discusses outcomes")
+ .tags(List.of("dice", "games", "random"))
+ .examples(
+ List.of("Can you roll a 6-sided die?"))
+ .build(),
+ new AgentSkill.Builder()
+ .id("prime_checker")
+ .name("Check prime numbers")
+ .description("Checks if given numbers are prime")
+ .tags(List.of("math", "prime", "numbers"))
+ .examples(
+ List.of(
+ "Is 17 a prime number?",
+ "Which of these numbers are prime: 1, 4, 6, 7"))
+ .build()))
+ .protocolVersion("0.3.0")
+ .additionalInterfaces(
+ List.of(
+ new AgentInterface(TransportProtocol.GRPC.asString(),
+ "localhost:" + httpPort),
+ new AgentInterface(
+ TransportProtocol.JSONRPC.asString(),
+ "http://localhost:" + httpPort)))
+ .build();
+ }
+}
diff --git a/samples/java/agents/dice_agent_multi_transport/server/src/main/java/com/samples/a2a/DiceAgentExecutorProducer.java b/samples/java/agents/dice_agent_multi_transport/server/src/main/java/com/samples/a2a/DiceAgentExecutorProducer.java
new file mode 100644
index 00000000..46c5a6f0
--- /dev/null
+++ b/samples/java/agents/dice_agent_multi_transport/server/src/main/java/com/samples/a2a/DiceAgentExecutorProducer.java
@@ -0,0 +1,111 @@
+package com.samples.a2a;
+
+import io.a2a.server.agentexecution.AgentExecutor;
+import io.a2a.server.agentexecution.RequestContext;
+import io.a2a.server.events.EventQueue;
+import io.a2a.server.tasks.TaskUpdater;
+import io.a2a.spec.JSONRPCError;
+import io.a2a.spec.Message;
+import io.a2a.spec.Part;
+import io.a2a.spec.Task;
+import io.a2a.spec.TaskNotCancelableError;
+import io.a2a.spec.TaskState;
+import io.a2a.spec.TextPart;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Produces;
+import jakarta.inject.Inject;
+import java.util.List;
+
+/** Producer for dice agent executor. */
+@ApplicationScoped
+public final class DiceAgentExecutorProducer {
+
+ /** The dice agent instance. */
+ @Inject private DiceAgent diceAgent;
+
+ /**
+ * Produces the agent executor for the dice agent.
+ *
+ * @return the configured agent executor
+ */
+ @Produces
+ public AgentExecutor agentExecutor() {
+ return new DiceAgentExecutor(diceAgent);
+ }
+
+ /** Dice agent executor implementation. */
+ private static class DiceAgentExecutor implements AgentExecutor {
+
+ /** The dice agent instance. */
+ private final DiceAgent agent;
+
+ /**
+ * Constructor for DiceAgentExecutor.
+ *
+ * @param diceAgentInstance the dice agent instance
+ */
+ DiceAgentExecutor(final DiceAgent diceAgentInstance) {
+ this.agent = diceAgentInstance;
+ }
+
+ @Override
+ public void execute(final RequestContext context,
+ final EventQueue eventQueue)
+ throws JSONRPCError {
+ final TaskUpdater updater = new TaskUpdater(context, eventQueue);
+
+ // mark the task as submitted and start working on it
+ if (context.getTask() == null) {
+ updater.submit();
+ }
+ updater.startWork();
+
+ // extract the text from the message
+ final String assignment = extractTextFromMessage(context.getMessage());
+
+ // call the dice agent with the message
+ final String response = agent.rollAndAnswer(assignment);
+
+ // create the response part
+ final TextPart responsePart = new TextPart(response, null);
+ final List> parts = List.of(responsePart);
+
+ // add the response as an artifact and complete the task
+ updater.addArtifact(parts, null, null, null);
+ updater.complete();
+ }
+
+ private String extractTextFromMessage(final Message message) {
+ final StringBuilder textBuilder = new StringBuilder();
+ if (message.getParts() != null) {
+ for (final Part> part : message.getParts()) {
+ if (part instanceof TextPart textPart) {
+ textBuilder.append(textPart.getText());
+ }
+ }
+ }
+ return textBuilder.toString();
+ }
+
+ @Override
+ public void cancel(final RequestContext context,
+ final EventQueue eventQueue)
+ throws JSONRPCError {
+ final Task task = context.getTask();
+
+ if (task.getStatus().state() == TaskState.CANCELED) {
+ // task already cancelled
+ throw new TaskNotCancelableError();
+ }
+
+ if (task.getStatus().state() == TaskState.COMPLETED) {
+ // task already completed
+ throw new TaskNotCancelableError();
+ }
+
+ // cancel the task
+ final TaskUpdater updater = new TaskUpdater(context, eventQueue);
+ updater.cancel();
+ }
+ }
+}
diff --git a/samples/java/agents/dice_agent_multi_transport/server/src/main/java/com/samples/a2a/DiceTools.java b/samples/java/agents/dice_agent_multi_transport/server/src/main/java/com/samples/a2a/DiceTools.java
new file mode 100644
index 00000000..0c15886f
--- /dev/null
+++ b/samples/java/agents/dice_agent_multi_transport/server/src/main/java/com/samples/a2a/DiceTools.java
@@ -0,0 +1,78 @@
+package com.samples.a2a;
+
+import dev.langchain4j.agent.tool.Tool;
+import jakarta.enterprise.context.ApplicationScoped;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+/** Service class that provides dice rolling and prime number functionality. */
+@ApplicationScoped
+public class DiceTools {
+
+ /** For generating rolls. */
+ private final Random random = new Random();
+
+ /** Default number of sides to use. */
+ private static final int DEFAULT_NUM_SIDES = 6;
+
+ /**
+ * Rolls an N sided dice. If number of sides aren't given, uses 6.
+ *
+ * @param n the number of the side of the dice to roll
+ * @return A number between 1 and N, inclusive
+ */
+ @Tool("Rolls an n sided dice. If number of sides aren't given, uses 6.")
+ public int rollDice(final int n) {
+ int sides = n;
+ if (sides <= 0) {
+ sides = DEFAULT_NUM_SIDES; // Default to 6 sides if invalid input
+ }
+ return random.nextInt(sides) + 1;
+ }
+
+ /**
+ * Check if a given list of numbers are prime.
+ *
+ * @param nums The list of numbers to check
+ * @return A string indicating which number is prime
+ */
+ @Tool("Check if a given list of numbers are prime.")
+ public String checkPrime(final List nums) {
+ Set primes = new HashSet<>();
+
+ for (Integer number : nums) {
+ if (number == null) {
+ continue;
+ }
+
+ int num = number.intValue();
+ if (num <= 1) {
+ continue;
+ }
+
+ boolean isPrime = true;
+ for (int i = 2; i <= Math.sqrt(num); i++) {
+ if (num % i == 0) {
+ isPrime = false;
+ break;
+ }
+ }
+
+ if (isPrime) {
+ primes.add(num);
+ }
+ }
+
+ if (primes.isEmpty()) {
+ return "No prime numbers found.";
+ } else {
+ return primes.stream()
+ .sorted()
+ .map(String::valueOf)
+ .collect(java.util.stream.Collectors.joining(", "))
+ + " are prime numbers.";
+ }
+ }
+}
diff --git a/samples/java/agents/dice_agent_multi_transport/server/src/main/java/com/samples/a2a/package-info.java b/samples/java/agents/dice_agent_multi_transport/server/src/main/java/com/samples/a2a/package-info.java
new file mode 100644
index 00000000..502874d1
--- /dev/null
+++ b/samples/java/agents/dice_agent_multi_transport/server/src/main/java/com/samples/a2a/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Dice Agent package.
+ */
+package com.samples.a2a;
diff --git a/samples/java/agents/dice_agent_multi_transport/server/src/main/resources/application.properties b/samples/java/agents/dice_agent_multi_transport/server/src/main/resources/application.properties
new file mode 100644
index 00000000..631da733
--- /dev/null
+++ b/samples/java/agents/dice_agent_multi_transport/server/src/main/resources/application.properties
@@ -0,0 +1,4 @@
+# Use the same port for gRPC and HTTP
+quarkus.grpc.server.use-separate-server=false
+quarkus.http.port=11000
+quarkus.langchain4j.ai.gemini.timeout=40000
diff --git a/samples/python/agents/dice_agent_grpc/test_client.py b/samples/python/agents/dice_agent_grpc/test_client.py
index a5e02a49..038121b1 100644
--- a/samples/python/agents/dice_agent_grpc/test_client.py
+++ b/samples/python/agents/dice_agent_grpc/test_client.py
@@ -46,15 +46,18 @@ async def main(agent_card_url: str, grpc_endpoint: str | None) -> None:
# specifies if authenticated card should be fetched.
# If an authenticated agent card is provided, client should use it for interacting with the gRPC service
try:
- logger.info(
- 'Attempting to fetch authenticated agent card from grpc endpoint'
- )
- proto_card = await stub.GetAgentCard(a2a_pb2.GetAgentCardRequest())
- logger.info('Successfully fetched agent card:')
- logger.info(proto_card)
- final_agent_card_to_use = proto_utils.FromProto.agent_card(
- proto_card
- )
+ if agent_card.supports_authenticated_extended_card:
+ logger.info(
+ 'Attempting to fetch authenticated agent card from grpc endpoint'
+ )
+ proto_card = await stub.GetAgentCard(a2a_pb2.GetAgentCardRequest())
+ logger.info('Successfully fetched agent card:')
+ logger.info(proto_card)
+ final_agent_card_to_use = proto_utils.FromProto.agent_card(
+ proto_card
+ )
+ else:
+ final_agent_card_to_use = agent_card
except Exception:
logging.exception('Failed to get authenticated agent card. Exiting.')
return
From e6690c287ce17f65ad5a4fa6febfe0b21a2e14d7 Mon Sep 17 00:00:00 2001
From: Farah Juma
Date: Fri, 26 Sep 2025 11:53:02 -0400
Subject: [PATCH 04/14] fix: Explicitly set the Gemini chat model id for the
Java agents (#373)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Description
This PR explicitly sets the Gemini chat model to use for the Java
agents. The agents were previously relying on `gemini-1.5.-flash`, the
[default Gemini chat
model](https://github.com/quarkiverse/quarkus-langchain4j/issues/1819)
set by Quarkus LangChain4j, but this chat model has been discontinued so
updating these agents to use a newer chat model instead.
Thank you for opening a Pull Request!
Before submitting your PR, there are a few things you can do to make
sure it goes smoothly:
- [x] Follow the [`CONTRIBUTING`
Guide](https://github.com/a2aproject/a2a-samples/blob/main/CONTRIBUTING.md).
Fixes # 🦕
---
.../content_editor/src/main/resources/application.properties | 3 ++-
.../content_writer/src/main/resources/application.properties | 3 ++-
.../server/src/main/resources/application.properties | 2 ++
.../weather_mcp/src/main/resources/application.properties | 3 ++-
4 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/samples/java/agents/content_editor/src/main/resources/application.properties b/samples/java/agents/content_editor/src/main/resources/application.properties
index 4f588190..be82040f 100644
--- a/samples/java/agents/content_editor/src/main/resources/application.properties
+++ b/samples/java/agents/content_editor/src/main/resources/application.properties
@@ -1,2 +1,3 @@
quarkus.http.port=10003
-quarkus.langchain4j.ai.gemini.timeout=40000
\ No newline at end of file
+quarkus.langchain4j.ai.gemini.timeout=40000
+quarkus.langchain4j.ai.gemini.chat-model.model-id=gemini-2.5-flash
diff --git a/samples/java/agents/content_writer/src/main/resources/application.properties b/samples/java/agents/content_writer/src/main/resources/application.properties
index 22aec47e..0b9a8222 100644
--- a/samples/java/agents/content_writer/src/main/resources/application.properties
+++ b/samples/java/agents/content_writer/src/main/resources/application.properties
@@ -1,2 +1,3 @@
quarkus.http.port=10002
-quarkus.langchain4j.ai.gemini.timeout=40000
\ No newline at end of file
+quarkus.langchain4j.ai.gemini.timeout=40000
+quarkus.langchain4j.ai.gemini.chat-model.model-id=gemini-2.5-flash
diff --git a/samples/java/agents/dice_agent_multi_transport/server/src/main/resources/application.properties b/samples/java/agents/dice_agent_multi_transport/server/src/main/resources/application.properties
index 631da733..b36c8790 100644
--- a/samples/java/agents/dice_agent_multi_transport/server/src/main/resources/application.properties
+++ b/samples/java/agents/dice_agent_multi_transport/server/src/main/resources/application.properties
@@ -2,3 +2,5 @@
quarkus.grpc.server.use-separate-server=false
quarkus.http.port=11000
quarkus.langchain4j.ai.gemini.timeout=40000
+quarkus.langchain4j.ai.gemini.chat-model.model-id=gemini-2.5-flash
+
diff --git a/samples/java/agents/weather_mcp/src/main/resources/application.properties b/samples/java/agents/weather_mcp/src/main/resources/application.properties
index 5e1433e9..a4d0cb3c 100644
--- a/samples/java/agents/weather_mcp/src/main/resources/application.properties
+++ b/samples/java/agents/weather_mcp/src/main/resources/application.properties
@@ -1,3 +1,4 @@
quarkus.http.port=10001
quarkus.langchain4j.mcp.weather.transport-type=stdio
-quarkus.langchain4j.mcp.weather.command=uv,--directory,mcp,run,weather_mcp.py
\ No newline at end of file
+quarkus.langchain4j.mcp.weather.command=uv,--directory,mcp,run,weather_mcp.py
+quarkus.langchain4j.ai.gemini.chat-model.model-id=gemini-2.5-flash
From c3efbe4f2a57e3c6f8426ac5f4ceb7da05f158a3 Mon Sep 17 00:00:00 2001
From: Farah Juma
Date: Fri, 26 Sep 2025 11:53:18 -0400
Subject: [PATCH 05/14] fix: Remove unnecessary dependency for the weather_mcp
Java agent (#371)
# Description
This PRs removes a dependency that isn't actually needed for the
`weather_mcp` Java agent:
Thank you for opening a Pull Request!
Before submitting your PR, there are a few things you can do to make
sure it goes smoothly:
- [x] Follow the [`CONTRIBUTING`
Guide](https://github.com/a2aproject/a2a-samples/blob/main/CONTRIBUTING.md).
---
samples/java/agents/weather_mcp/pom.xml | 12 ------------
1 file changed, 12 deletions(-)
diff --git a/samples/java/agents/weather_mcp/pom.xml b/samples/java/agents/weather_mcp/pom.xml
index b6b7e35e..7d4664c7 100644
--- a/samples/java/agents/weather_mcp/pom.xml
+++ b/samples/java/agents/weather_mcp/pom.xml
@@ -59,18 +59,6 @@
quarkus-langchain4j-mcp
${quarkus.langchain4j.version}
-
- io.quarkiverse.langchain4j
- quarkus-langchain4j-mcp-deployment
- ${quarkus.langchain4j.version}
- pom
-
-
- *
- *
-
-
-
From f53b6a565c4e59335ed1fbf28a41a1ec1fba9043 Mon Sep 17 00:00:00 2001
From: gulliantonio <167304324+gulliantonio@users.noreply.github.com>
Date: Mon, 29 Sep 2025 22:42:33 +0200
Subject: [PATCH 06/14] feat: Add secure-passport sample from google-octo/ag
gulli@google.com (#370)
This proposal extends the A2A protocol with a lightweight "Secure
Passport" feature, enabling calling agents to share verified information
(e.g., user preferences, session history, tool access permissions ..)
with contacted agents. This enhances agent interactions from anonymous
transactions to trusted, context-aware partnerships, improving
reliability and personalization within the agent ecosystem. A reference
implementation is provided. Go Link:
[go/a2a-secure-passport](http://goto.google.com/a2a-secure-passport)
---------
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Holt Skinner <13262395+holtskinner@users.noreply.github.com>
---
extensions/secure-passport/HOWTORUN.md | 97 +++++
extensions/secure-passport/README.md | 23 ++
.../v1/samples/python/README.md | 148 ++++++++
.../v1/samples/python/poetry.lock | 334 ++++++++++++++++++
.../v1/samples/python/pyproject.toml | 25 ++
.../secure-passport/v1/samples/python/run.py | 125 +++++++
.../src/secure_passport_ext/__init__.py | 108 ++++++
.../python/src/secure_passport_ext/py.typed | 1 +
.../python/tests/test_secure_passport.py | 186 ++++++++++
extensions/secure-passport/v1/spec.md | 133 +++++++
10 files changed, 1180 insertions(+)
create mode 100644 extensions/secure-passport/HOWTORUN.md
create mode 100644 extensions/secure-passport/README.md
create mode 100644 extensions/secure-passport/v1/samples/python/README.md
create mode 100644 extensions/secure-passport/v1/samples/python/poetry.lock
create mode 100644 extensions/secure-passport/v1/samples/python/pyproject.toml
create mode 100644 extensions/secure-passport/v1/samples/python/run.py
create mode 100644 extensions/secure-passport/v1/samples/python/src/secure_passport_ext/__init__.py
create mode 100644 extensions/secure-passport/v1/samples/python/src/secure_passport_ext/py.typed
create mode 100644 extensions/secure-passport/v1/samples/python/tests/test_secure_passport.py
create mode 100644 extensions/secure-passport/v1/spec.md
diff --git a/extensions/secure-passport/HOWTORUN.md b/extensions/secure-passport/HOWTORUN.md
new file mode 100644
index 00000000..0b20cbf9
--- /dev/null
+++ b/extensions/secure-passport/HOWTORUN.md
@@ -0,0 +1,97 @@
+# HOW TO RUN the Secure Passport Extension Sample
+
+This guide provides step-by-step instructions for setting up the environment and running the Python sample code for the **Secure Passport Extension v1**.
+
+The sample is located in the `samples/python/` directory.
+
+---
+
+## 1. Prerequisites
+
+You need the following installed on your system:
+
+* **Python** (version 3.9 or higher)
+* **Poetry** (Recommended for dependency management via `pyproject.toml`)
+
+---
+
+## 2. Setup and Installation
+
+1. **From the repository root, navigate** to the sample project directory:
+ ```bash
+ cd extensions/secure-passport/v1/samples/python
+ ```
+
+2. **Install Dependencies** using Poetry. This command reads `pyproject.toml`, creates a virtual environment, and installs `pydantic` and `pytest`.
+ ```bash
+ poetry install
+ ```
+
+3. **Activate** the virtual environment:
+ ```bash
+ poetry shell
+ ```
+
+ *(Note: All subsequent commands are run from within this activated environment.)*
+
+---
+
+## 3. Execution
+
+There are two ways to run the code: using the automated unit tests or using a manual script.
+
+### A. Run Unit Tests (Recommended)
+
+Running the tests is the most complete way to verify the extension's data modeling, integrity checks, and validation logic.
+
+```bash
+# Execute Pytest against the test directory
+pytest tests/
+
+### B. Run Middleware Demo Script
+
+Execute `run.py` to see the full client/server middleware pipeline in action for all four use cases:
+
+```bash
+python run.py
+
+### Expected Console Output
+
+The output below demonstrates the successful execution of the four use cases via the simulated middleware pipeline:
+
+========================================================= Secure Passport Extension Demo (Middleware)
+--- Use Case: Efficient Currency Conversion (via Middleware) ---
+[PIPELINE] Client Side: Middleware -> Transport
+[Middleware: Client] Attaching Secure Passport for a2a://travel-orchestrator.com
+[Transport] Message sent over the wire.
+[PIPELINE] Server Side: Middleware -> Agent Core
+[Middleware: Server] Extracted Secure Passport. Verified: True
+[Agent Core] Task received for processing.
+[Agent Core] Executing task with verified context: Currency=GBP, Tier=Silver
+
+--- Use Case: Personalized Travel Booking (via Middleware) ---
+[PIPELINE] Client Side: Middleware -> Transport
+[Middleware: Client] Attaching Secure Passport for a2a://travel-portal.com
+[Transport] Message sent over the wire.
+[PIPELINE] Server Side: Middleware -> Agent Core
+[Middleware: Server] Extracted Secure Passport. Verified: True
+[Agent Core] Task received for processing.
+[Agent Core] Executing task with verified context: Currency=Unknown, Tier=Platinum
+
+--- Use Case: Proactive Retail Assistance (via Middleware) ---
+[PIPELINE] Client Side: Middleware -> Transport
+[Middleware: Client] Attaching Secure Passport for a2a://ecommerce-front.com
+[Transport] Message sent over the wire.
+[PIPELINE] Server Side: Middleware -> Agent Core
+[Middleware: Server] Extracted Secure Passport. Verified: False
+[Agent Core] Task received for processing.
+[Agent Core] Executing task with unverified context (proceeding cautiously).
+
+--- Use Case: Marketing Agent seek insights (via Middleware) ---
+[PIPELINE] Client Side: Middleware -> Transport
+[Middleware: Client] Attaching Secure Passport for a2a://marketing-agent.com
+[Transport] Message sent over the wire.
+[PIPELINE] Server Side: Middleware -> Agent Core
+[Middleware: Server] Extracted Secure Passport. Verified: True
+[Agent Core] Task received for processing.
+[Agent Core] Executing task with verified context: Currency=Unknown, Tier=Standard
diff --git a/extensions/secure-passport/README.md b/extensions/secure-passport/README.md
new file mode 100644
index 00000000..14712b39
--- /dev/null
+++ b/extensions/secure-passport/README.md
@@ -0,0 +1,23 @@
+# Secure Passport Extension
+
+This directory contains the specification and a Python sample implementation for the **Secure Passport Extension v1** for the Agent2Agent (A2A) protocol.
+
+## Purpose
+
+The Secure Passport extension introduces a **trusted, contextual layer** for A2A communication. It allows a calling agent to securely and voluntarily share a structured subset of its current contextual state with the callee agent. This is designed to transform anonymous, transactional calls into collaborative partnerships, enabling:
+
+* **Immediate Personalization:** Specialist agents can use context (like loyalty tier or preferred currency) immediately.
+* **Reduced Overhead:** Eliminates the need for multi-turn conversations to establish context.
+* **Enhanced Trust:** Includes a **`signature`** field for cryptographic verification of the data's origin and integrity.
+
+## Specification
+
+The full technical details, including data models, required fields, and security considerations, are documented here:
+
+➡️ **[Full Specification (v1)](./v1/spec.md)**
+
+## Sample Implementation
+
+A runnable example demonstrating the implementation of the `CallerContext` data model and the utility functions for integration with the A2A SDK is provided in the `samples` directory.
+
+➡️ **[Python Sample Usage Guide](./v1/samples/python/README.md)**
\ No newline at end of file
diff --git a/extensions/secure-passport/v1/samples/python/README.md b/extensions/secure-passport/v1/samples/python/README.md
new file mode 100644
index 00000000..e2de5a1c
--- /dev/null
+++ b/extensions/secure-passport/v1/samples/python/README.md
@@ -0,0 +1,148 @@
+# Secure Passport Python Sample
+
+This sample provides the runnable code for the **Secure Passport Extension v1** for the Agent2Agent (A2A) protocol, demonstrating its implementation and usage in a Python environment.
+
+## 1. Extension Overview
+
+The core of this extension is the **`CallerContext`** data model, which is attached to the A2A message metadata under the extension's unique URI. This enables the secure transfer of trusted contextual state between collaborating agents.
+
+### Extension URI
+
+The unique identifier for this extension is:
+`https://github.com/a2aproject/a2a-samples/tree/main/samples/python/extensions/secure-passport`
+
+---
+
+## 2. Comprehensive Usage and Middleware Demonstration
+
+The `run.py` script demonstrates the full client-server pipeline using the conceptual **middleware layers** for seamless integration.
+
+### A. Use Case Code Demonstration
+
+The following code demonstrates how to create the specific `CallerContext` payloads for the four core use cases, verifying that the structure and integrity checks work as intended.
+
+```python
+from secure_passport_ext import (
+ CallerContext,
+ A2AMessage,
+ add_secure_passport,
+ get_secure_passport
+)
+
+def demonstrate_use_case(title: str, client_id: str, state: dict, signature: str | None = None, session_id: str | None = None):
+ print(f"\n--- Demonstrating: {title} ---")
+
+ passport = CallerContext(
+ client_id=client_id,
+ session_id=session_id,
+ signature=signature,
+ state=state
+ )
+
+ message = A2AMessage()
+ add_secure_passport(message, passport)
+ retrieved = get_secure_passport(message)
+
+ if retrieved:
+ print(f" Source: {retrieved.client_id}")
+ print(f" Verified: {retrieved.is_verified}")
+ print(f" Context: {retrieved.state}")
+ else:
+ print(" [ERROR] Passport retrieval failed.")
+
+# 1. Efficient Currency Conversion (Low Context, High Trust)
+
+demonstrate_use_case(
+ title="1. Currency Conversion (GBP)",
+ client_id="a2a://travel-orchestrator.com",
+ state={"user_preferred_currency": "GBP", "user_id": "U001"},
+ signature="sig-currency-1"
+)
+
+# 2. Personalized Travel Booking (High Context, Session Data)
+
+demonstrate_use_case(
+ title="2. Personalized Travel (Platinum Tier)",
+ client_id="a2a://travel-portal.com",
+ session_id="travel-session-999",
+ state={
+ "destination": "Bali, Indonesia",
+ "loyalty_tier": "Platinum"
+ },
+ signature="sig-travel-2"
+)
+
+# 3. Proactive Retail Assistance (Unsigned, Quick Context)
+
+demonstrate_use_case(
+ title="3. Retail Assistance (Unverified)",
+ client_id="a2a://ecommerce-front.com",
+ state={"product_sku": "Nikon-Z-50mm-f1.8", "user_intent": "seeking_reviews"},
+ signature=None
+)
+
+# 4. Marketing Agent seek insights (High Trust, Secured Scope)
+
+demonstrate_use_case(
+ title="4. Secured DB Access (Finance)",
+ client_id="a2a://marketing-agent.com",
+ state={
+ "query_type": "quarterly_revenue",
+ "access_scope": ["read:finance_db", "user:Gulli"]
+ },
+ signature="sig-finance-4"
+)
+```
+
+### B. Convenience Method: AgentCard Declaration
+
+The `SecurePassportExtension` class provides a static method to easily generate the necessary JSON structure for including this extension in an agent's `AgentCard`. This ensures the structure is always compliant.
+
+```python
+from secure_passport_ext import SecurePassportExtension
+
+# Scenario 1: Agent supports basic Secure Passport
+simple_declaration = SecurePassportExtension.get_agent_card_declaration()
+# Output will be: {'uri': '...', 'params': {'receivesCallerContext': True}}
+
+# Scenario 2: Agent supports specific keys (e.g., the Travel Agent)
+travel_keys = ["destination", "loyalty_tier", "dates"]
+complex_declaration = SecurePassportExtension.get_agent_card_declaration(travel_keys)
+# Output will include: 'supportedStateKeys': ['destination', 'loyalty_tier', 'dates']
+```
+
+## 3. How to Run the Sample 🚀
+
+To run the sample and execute the comprehensive unit tests, follow these steps.
+
+### A. Setup and Installation
+
+1. **Navigate** to the Python sample directory:
+ ```bash
+ cd extensions/secure-passport/v1/samples/python
+ ```
+2. **Install Dependencies** (using Poetry):
+ ```bash
+ poetry install
+
+ # Activate the virtual environment
+ poetry shell
+ ```
+
+### B. Verification and Execution
+
+#### 1. Run Unit Tests (Recommended)
+
+Confirm all 11 core logic and validation tests pass:
+
+```bash
+pytest tests/
+```
+
+#### 2. Run Middleware Demo Script
+
+Execute `run.py` to see the full client/server middleware pipeline in action for all four use cases:
+
+```bash
+python run.py
+```
diff --git a/extensions/secure-passport/v1/samples/python/poetry.lock b/extensions/secure-passport/v1/samples/python/poetry.lock
new file mode 100644
index 00000000..1287032e
--- /dev/null
+++ b/extensions/secure-passport/v1/samples/python/poetry.lock
@@ -0,0 +1,334 @@
+# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+description = "Reusable constraint types to use with typing.Annotated"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
+ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+groups = ["dev"]
+markers = "sys_platform == \"win32\""
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.0"
+description = "Backport of PEP 654 (exception groups)"
+optional = false
+python-versions = ">=3.7"
+groups = ["dev"]
+markers = "python_version < \"3.11\""
+files = [
+ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"},
+ {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"},
+]
+
+[package.dependencies]
+typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""}
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "iniconfig"
+version = "2.1.0"
+description = "brain-dead simple config-ini parsing"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
+ {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
+ {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+description = "plugin and hook calling mechanisms for python"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
+ {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
+]
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["coverage", "pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pydantic"
+version = "2.11.9"
+description = "Data validation using Python type hints"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2"},
+ {file = "pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2"},
+]
+
+[package.dependencies]
+annotated-types = ">=0.6.0"
+pydantic-core = "2.33.2"
+typing-extensions = ">=4.12.2"
+typing-inspection = ">=0.4.0"
+
+[package.extras]
+email = ["email-validator (>=2.0.0)"]
+timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""]
+
+[[package]]
+name = "pydantic-core"
+version = "2.33.2"
+description = "Core functionality for Pydantic validation and serialization"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"},
+ {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"},
+ {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"},
+ {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"},
+ {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"},
+ {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"},
+ {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"},
+ {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"},
+ {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"},
+ {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"},
+ {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"},
+ {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"},
+ {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"},
+ {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"},
+ {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"},
+ {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"},
+ {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"},
+ {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"},
+ {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"},
+ {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"},
+ {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"},
+ {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"},
+ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"},
+ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"},
+ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"},
+ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"},
+ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"},
+ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"},
+ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"},
+ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"},
+ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"},
+ {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+description = "Pygments is a syntax highlighting package written in Python."
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
+ {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
+]
+
+[package.extras]
+windows-terminal = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "pytest"
+version = "8.4.2"
+description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"},
+ {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"},
+]
+
+[package.dependencies]
+colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""}
+iniconfig = ">=1"
+packaging = ">=20"
+pluggy = ">=1.5,<2"
+pygments = ">=2.7.2"
+tomli = {version = ">=1", markers = "python_version < \"3.11\""}
+
+[package.extras]
+dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "tomli"
+version = "2.2.1"
+description = "A lil' TOML parser"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+markers = "python_version < \"3.11\""
+files = [
+ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
+ {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
+ {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
+ {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
+ {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
+ {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
+ {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
+ {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
+ {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
+ {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
+ {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
+ {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
+ {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
+ {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+description = "Backported and Experimental Type Hints for Python 3.9+"
+optional = false
+python-versions = ">=3.9"
+groups = ["main", "dev"]
+files = [
+ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
+ {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
+]
+markers = {dev = "python_version < \"3.11\""}
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.1"
+description = "Runtime typing introspection tools"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"},
+ {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.12.0"
+
+[metadata]
+lock-version = "2.1"
+python-versions = "^3.9"
+content-hash = "cd3dd12d740734da980a168ae2b4bbe435aa15f03376e722545d5592844e475f"
diff --git a/extensions/secure-passport/v1/samples/python/pyproject.toml b/extensions/secure-passport/v1/samples/python/pyproject.toml
new file mode 100644
index 00000000..1106f9da
--- /dev/null
+++ b/extensions/secure-passport/v1/samples/python/pyproject.toml
@@ -0,0 +1,25 @@
+# pyproject.toml
+
+[tool.poetry]
+name = "secure-passport-ext"
+version = "1.0.0"
+description = "A2A Protocol Extension Sample: Secure Passport for Contextual State Sharing"
+authors = ["Google Octo "]
+license = "Apache-2.0"
+
+# --- FIX: 'packages' is a key directly under [tool.poetry] ---
+packages = [
+ { include = "secure_passport_ext", from = "src" }
+]
+# -----------------------------------------------------------
+
+[tool.poetry.dependencies]
+python = "^3.9"
+pydantic = "^2.0.0"
+
+[tool.poetry.group.dev.dependencies]
+pytest = "^8.0.0"
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
\ No newline at end of file
diff --git a/extensions/secure-passport/v1/samples/python/run.py b/extensions/secure-passport/v1/samples/python/run.py
new file mode 100644
index 00000000..5337684d
--- /dev/null
+++ b/extensions/secure-passport/v1/samples/python/run.py
@@ -0,0 +1,125 @@
+# run.py
+
+from secure_passport_ext import (
+ CallerContext,
+ A2AMessage, # CORRECTED: Importing the standardized A2AMessage type
+ SecurePassportExtension # Import the extension utility class
+)
+
+# --- Define Mock Handlers for the Pipeline ---
+
+def mock_transport_send(message: A2AMessage): # CORRECTED: Signature uses A2AMessage
+ """Mocks the final step of the client sending the message over the wire."""
+ print(" [Transport] Message sent over the wire.")
+ return message # Returns the message the server would receive
+
+def mock_agent_core_handler(message: A2AMessage, passport: CallerContext | None): # CORRECTED: Signature uses A2AMessage
+ """
+ Mocks the agent's core logic, which receives context from the Server Middleware.
+ """
+ print(" [Agent Core] Task received for processing.")
+
+ if passport and passport.is_verified:
+ # NOTE: Accessing the context attributes with snake_case
+ currency = passport.state.get("user_preferred_currency", "Unknown")
+ tier = passport.state.get("loyalty_tier", "Standard")
+ print(f" [Agent Core] Executing task with verified context: Currency={currency}, Tier={tier}")
+ elif passport and not passport.is_verified:
+ print(" [Agent Core] Executing task with unverified context (proceeding cautiously).")
+ else:
+ print(" [Agent Core] Executing task with no external context.")
+
+
+def create_and_run_passport_test(client_id: str, session_id: str | None, state: dict, signature: str | None, use_case_title: str):
+ """
+ Demonstrates a full communication cycle using the conceptual middleware.
+ """
+
+ print(f"\n--- Use Case: {use_case_title} (via Middleware) ---")
+
+ # 1. Orchestrator (Client) creates the Passport
+ client_passport = CallerContext(
+ client_id=client_id,
+ session_id=session_id,
+ signature=signature,
+ state=state
+ )
+
+ # Mock A2A Message Container
+ client_message = A2AMessage()
+
+ # --- CLIENT-SIDE PIPELINE ---
+ print(" [PIPELINE] Client Side: Middleware -> Transport")
+
+ message_over_wire = SecurePassportExtension.client_middleware(
+ next_handler=mock_transport_send,
+ message=client_message,
+ context=client_passport
+ )
+
+ # --- SERVER-SIDE PIPELINE ---
+ print(" [PIPELINE] Server Side: Middleware -> Agent Core")
+
+ # Server Middleware is executed, wrapping the Agent Core Handler.
+ SecurePassportExtension.server_middleware(
+ next_handler=mock_agent_core_handler,
+ message=message_over_wire
+ )
+
+
+def run_all_samples():
+ print("=========================================================")
+ print(" Secure Passport Extension Demo (Middleware)")
+ print("=========================================================")
+
+ # --- Use Case 1: Efficient Currency Conversion (High Trust Example) ---
+ create_and_run_passport_test(
+ client_id="a2a://travel-orchestrator.com",
+ session_id=None,
+ state={"user_preferred_currency": "GBP", "loyalty_tier": "Silver"},
+ signature="sig-currency-1",
+ use_case_title="Efficient Currency Conversion"
+ )
+
+ # --- Use Case 2: Personalized Travel Booking (High Context Example) ---
+ create_and_run_passport_test(
+ client_id="a2a://travel-portal.com",
+ session_id="travel-booking-session-999",
+ state={
+ "destination": "Bali, Indonesia",
+ "loyalty_tier": "Platinum"
+ },
+ signature="sig-travel-2",
+ use_case_title="Personalized Travel Booking"
+ )
+
+ # --- Use Case 3: Proactive Retail Assistance (Unsigned/Low Trust Example) ---
+ create_and_run_passport_test(
+ client_id="a2a://ecommerce-front.com",
+ session_id="cart-session-404",
+ state={
+ "product_sku": "Nikon-Z-50mm-f1.8",
+ "user_intent": "seeking_reviews"
+ },
+ signature=None, # Explicitly missing signature
+ use_case_title="Proactive Retail Assistance"
+ )
+
+ # --- Use Case 4: Marketing Agent seek insights (Secured Scope Example) ---
+ create_and_run_passport_test(
+ client_id="a2a://marketing-agent.com",
+ session_id=None,
+ state={
+ "query_type": "quarterly_revenue",
+ "access_scope": ["read:finance_db"]
+ },
+ signature="sig-finance-4",
+ use_case_title="Marketing Agent seek insights"
+ )
+
+
+if __name__ == "__main__":
+ run_all_samples()
+
+
+
\ No newline at end of file
diff --git a/extensions/secure-passport/v1/samples/python/src/secure_passport_ext/__init__.py b/extensions/secure-passport/v1/samples/python/src/secure_passport_ext/__init__.py
new file mode 100644
index 00000000..918396f4
--- /dev/null
+++ b/extensions/secure-passport/v1/samples/python/src/secure_passport_ext/__init__.py
@@ -0,0 +1,108 @@
+from typing import Optional, Dict, Any, List, Callable
+from pydantic import BaseModel, Field, ValidationError, ConfigDict
+from copy import deepcopy
+
+# --- Extension Definition ---
+
+SECURE_PASSPORT_URI = "https://github.com/a2aproject/a2a-samples/tree/main/samples/python/extensions/secure-passport"
+
+class CallerContext(BaseModel):
+ """
+ The Secure Passport payload containing contextual state shared by the calling agent.
+ """
+ # *** CORE CHANGE: agent_id renamed to client_id ***
+ client_id: str = Field(..., alias='clientId', description="The verifiable unique identifier of the calling client.")
+ signature: Optional[str] = Field(None, alias='signature', description="A cryptographic signature of the 'state' payload.")
+ session_id: Optional[str] = Field(None, alias='sessionId', description="A session or conversation identifier for continuity.")
+ state: Dict[str, Any] = Field(..., description="A free-form JSON object containing the contextual data.")
+
+ # Use ConfigDict for Pydantic V2 compatibility and configuration
+ model_config = ConfigDict(
+ populate_by_name=True,
+ extra='forbid'
+ )
+
+ @property
+ def is_verified(self) -> bool:
+ """
+ Conceptually checks if the passport contains a valid signature.
+ """
+ return self.signature is not None
+
+# --- Helper Functions (Core Protocol Interaction) ---
+
+class BaseA2AMessage(BaseModel):
+ metadata: Dict[str, Any] = Field(default_factory=dict)
+
+try:
+ from a2a.types import A2AMessage
+except ImportError:
+ A2AMessage = BaseA2AMessage
+
+def add_secure_passport(message: A2AMessage, context: CallerContext) -> None:
+ """Adds the Secure Passport (CallerContext) to the message's metadata."""
+
+ message.metadata[SECURE_PASSPORT_URI] = context.model_dump(by_alias=True, exclude_none=True)
+
+def get_secure_passport(message: A2AMessage) -> Optional[CallerContext]:
+ """Retrieves and validates the Secure Passport from the message metadata."""
+ passport_data = message.metadata.get(SECURE_PASSPORT_URI)
+ if not passport_data:
+ return None
+
+ try:
+ return CallerContext.model_validate(deepcopy(passport_data))
+ except ValidationError as e:
+ import logging
+ logging.warning(f"ERROR: Received malformed Secure Passport data. Ignoring payload: {e}")
+ return None
+
+# ======================================================================
+# Convenience and Middleware Concepts
+# ======================================================================
+
+class SecurePassportExtension:
+ """
+ A conceptual class containing static methods for extension utilities
+ and defining middleware layers for seamless integration.
+ """
+ @staticmethod
+ def get_agent_card_declaration(supported_state_keys: Optional[List[str]] = None) -> Dict[str, Any]:
+ """
+ Generates the JSON structure required to declare support for this
+ extension in an A2A AgentCard.
+ """
+ declaration = {
+ "uri": SECURE_PASSPORT_URI,
+ "params": {}
+ }
+ if supported_state_keys:
+ declaration["params"]["supportedStateKeys"] = supported_state_keys
+
+ return declaration
+
+ @staticmethod
+ def client_middleware(next_handler: Callable[[A2AMessage], Any], message: A2AMessage, context: CallerContext):
+ """
+ [Conceptual Middleware Layer: Client/Calling Agent]
+ """
+ # ACCESS UPDATED: Use context.client_id
+ print(f"[Middleware: Client] Attaching Secure Passport for {context.client_id}")
+ add_secure_passport(message, context)
+ return next_handler(message)
+
+ @staticmethod
+ def server_middleware(next_handler: Callable[[A2AMessage, Optional[CallerContext]], Any], message: A2AMessage):
+ """
+ [Conceptual Middleware Layer: Server/Receiving Agent]
+ """
+ passport = get_secure_passport(message)
+
+ if passport:
+ print(f"[Middleware: Server] Extracted Secure Passport. Verified: {passport.is_verified}")
+ else:
+ print("[Middleware: Server] No Secure Passport found or validation failed.")
+
+ return next_handler(message, passport)
+
+
diff --git a/extensions/secure-passport/v1/samples/python/src/secure_passport_ext/py.typed b/extensions/secure-passport/v1/samples/python/src/secure_passport_ext/py.typed
new file mode 100644
index 00000000..339827c3
--- /dev/null
+++ b/extensions/secure-passport/v1/samples/python/src/secure_passport_ext/py.typed
@@ -0,0 +1 @@
+# This file is intentionally left empty.
diff --git a/extensions/secure-passport/v1/samples/python/tests/test_secure_passport.py b/extensions/secure-passport/v1/samples/python/tests/test_secure_passport.py
new file mode 100644
index 00000000..b082b1d3
--- /dev/null
+++ b/extensions/secure-passport/v1/samples/python/tests/test_secure_passport.py
@@ -0,0 +1,186 @@
+import pytest
+from secure_passport_ext import (
+ CallerContext,
+ A2AMessage,
+ add_secure_passport,
+ get_secure_passport,
+ SECURE_PASSPORT_URI,
+)
+
+# ======================================================================
+## Fixtures for Core Tests
+# ======================================================================
+
+@pytest.fixture
+def valid_passport_data():
+ """
+ Returns a dictionary for creating a valid CallerContext.
+ Keys are snake_case to align with the final CallerContext model attributes.
+ """
+ return {
+ "client_id": "a2a://orchestrator.com", # CORRECTED: Changed agent_id to client_id
+ "session_id": "session-123",
+ "state": {"currency": "USD", "tier": "silver"},
+ "signature": "mock-signature-xyz"
+ }
+
+# ======================================================================
+## Core Functionality Tests
+# ======================================================================
+
+def test_add_and_get_passport_success(valid_passport_data):
+ """Tests successful serialization and deserialization in a round trip."""
+ passport = CallerContext(**valid_passport_data)
+ message = A2AMessage()
+
+ add_secure_passport(message, passport)
+ retrieved = get_secure_passport(message)
+
+ assert retrieved is not None
+ assert retrieved.client_id == "a2a://orchestrator.com" # CORRECTED: Access via client_id
+ assert retrieved.state == {"currency": "USD", "tier": "silver"}
+
+def test_get_passport_when_missing():
+ """Tests retrieving a passport from a message that doesn't have one."""
+ message = A2AMessage()
+ retrieved = get_secure_passport(message)
+ assert retrieved is None
+
+def test_passport_validation_failure_missing_required_field(valid_passport_data):
+ """Tests validation fails when a required field (client_id) is missing."""
+ invalid_data = valid_passport_data.copy()
+ del invalid_data['client_id'] # CORRECTED: Deleting client_id key
+
+ message = A2AMessage()
+ message.metadata[SECURE_PASSPORT_URI] = invalid_data
+
+ retrieved = get_secure_passport(message)
+ assert retrieved is None
+
+def test_passport_validation_failure_extra_field(valid_passport_data):
+ """Tests validation fails when an unknown field is present (due to extra='forbid')."""
+ invalid_data = valid_passport_data.copy()
+ invalid_data['extra_field'] = 'unsupported'
+
+ message = A2AMessage()
+ message.metadata[SECURE_PASSPORT_URI] = invalid_data
+
+ retrieved = get_secure_passport(message)
+ assert retrieved is None
+
+def test_passport_is_verified_with_signature(valid_passport_data):
+ """Tests that the is_verified property is True when a signature is present."""
+ passport = CallerContext(**valid_passport_data)
+ assert passport.is_verified is True
+
+def test_passport_is_unverified_without_signature(valid_passport_data):
+ """Tests that the is_verified property is False when the signature is missing."""
+ data_without_sig = valid_passport_data.copy()
+ data_without_sig['signature'] = None
+ passport = CallerContext(**data_without_sig)
+ assert passport.is_verified is False
+
+def test_retrieved_passport_is_immutable_from_message_data(valid_passport_data):
+ """Tests that modifying the retrieved copy's state does not change the original message metadata (due to deepcopy)."""
+ passport = CallerContext(**valid_passport_data)
+ message = A2AMessage()
+ add_secure_passport(message, passport)
+
+ retrieved = get_secure_passport(message)
+ retrieved.state['new_key'] = 'changed_value'
+
+ original_data = message.metadata[SECURE_PASSPORT_URI]['state']
+
+ assert 'new_key' not in original_data
+ assert original_data['currency'] == 'USD'
+
+
+# ======================================================================
+## Use Case Integration Tests
+# ======================================================================
+
+def test_use_case_1_currency_conversion():
+ """Verifies the structure for passing a user's currency preference."""
+ state_data = {
+ "user_preferred_currency": "GBP",
+ "user_id": "U001"
+ }
+
+ passport = CallerContext(
+ client_id="a2a://travel-orchestrator.com", # CORRECTED: Using client_id keyword
+ state=state_data,
+ signature="sig-currency-1"
+ )
+
+ message = A2AMessage()
+ add_secure_passport(message, passport)
+ retrieved = get_secure_passport(message)
+
+ assert retrieved.state.get("user_preferred_currency") == "GBP"
+ assert retrieved.is_verified is True
+
+def test_use_case_2_personalized_travel_booking():
+ """Verifies the structure for passing detailed session and loyalty data."""
+ state_data = {
+ "destination": "Bali, Indonesia",
+ "dates": "2025-12-01 to 2025-12-15",
+ "loyalty_tier": "Platinum"
+ }
+
+ passport = CallerContext(
+ client_id="a2a://travel-portal.com", # CORRECTED: Using client_id keyword
+ session_id="travel-booking-session-999",
+ state=state_data,
+ signature="sig-travel-2"
+ )
+
+ message = A2AMessage()
+ add_secure_passport(message, passport)
+ retrieved = get_secure_passport(message)
+
+ assert retrieved.session_id == "travel-booking-session-999"
+ assert retrieved.state.get("loyalty_tier") == "Platinum"
+ assert retrieved.is_verified is True
+
+def test_use_case_3_proactive_retail_assistance():
+ """Verifies the structure for passing product context for assistance."""
+ state_data = {
+ "product_sku": "Nikon-Z-50mm-f1.8",
+ "cart_status": "in_cart",
+ "user_intent": "seeking_reviews"
+ }
+
+ passport = CallerContext(
+ client_id="a2a://ecommerce-front.com", # CORRECTED: Using client_id keyword
+ state=state_data,
+ )
+
+ message = A2AMessage()
+ add_secure_passport(message, passport)
+ retrieved = get_secure_passport(message)
+
+ assert retrieved.state.get("product_sku") == "Nikon-Z-50mm-f1.8"
+ assert retrieved.is_verified is False
+ assert retrieved.session_id is None
+
+def test_use_case_4_secured_db_insights():
+ """Verifies the structure for passing required request arguments for a secured DB/ERP agent."""
+ state_data = {
+ "query_type": "quarterly_revenue",
+ "time_period": {"start": "2025-07-01", "end": "2025-09-30"},
+ "access_scope": ["read:finance_db", "user:Gulli"]
+ }
+
+ passport = CallerContext(
+ client_id="a2a://marketing-agent.com", # CORRECTED: Using client_id keyword
+ state=state_data,
+ signature="sig-finance-4"
+ )
+
+ message = A2AMessage()
+ add_secure_passport(message, passport)
+ retrieved = get_secure_passport(message)
+
+ assert retrieved.state.get("query_type") == "quarterly_revenue"
+ assert "read:finance_db" in retrieved.state.get("access_scope")
+ assert retrieved.is_verified is True
diff --git a/extensions/secure-passport/v1/spec.md b/extensions/secure-passport/v1/spec.md
new file mode 100644
index 00000000..0e8c984d
--- /dev/null
+++ b/extensions/secure-passport/v1/spec.md
@@ -0,0 +1,133 @@
+# A2A Protocol Extension: Secure Passport (v1)
+
+- **URI:** `https://github.com/a2aproject/a2a-samples/tree/main/samples/python/extensions/secure-passport`
+- **Type:** Profile Extension / Data-Only Extension
+- **Version:** 1.0.0
+
+## Abstract
+
+This extension enables an Agent2Agent (A2A) client to securely and optionally share a structured, verifiable contextual state—the **Secure Passport**—with the callee agent. This context is intended to transform anonymous A2A calls into trusted, context-aware partnerships.
+
+## 1. Structure and Flow Overview
+
+The Secure Passport is the core payload (`CallerContext`), which enables a simple, two-part request flow designed for efficiency and trust.
+
+### A. Primary Payload Fields and Significance
+
+The `CallerContext` object is placed in the message metadata and must contain the following fields:
+
+| Field | Significance |
+| :--- | :--- |
+| **`clientId`** | **Identity:** Uniquely identifies the client/agent originating the context. |
+| **`state`** | **Context:** Contains the custom, structured data needed to fulfill the request without further questions. |
+| **`signature`** | **Trust:** A digital signature over the `state`, allowing the receiver to cryptographically verify data integrity and origin. |
+
+### B. Expected Request Flow
+
+The extension defines two points of interaction (which should typically be handled by SDK middleware):
+
+1. **Client-Side (Attaching):** The client generates the `CallerContext` (including the signature, if required for high-trust) and inserts the entire payload into he A2A message's metadata map.
+2. **Server-Side (Extracting):** The callee agent extracts the `CallerContext` from the metadata, validates the signature, and uses the `state` object to execute the task.
+
+***
+
+## 2. Agent Declaration and Negotiation
+
+An A2A Agent that is capable of **receiving** and utilizing the Secure Passport context **MUST** declare its support in its `AgentCard` under the **`extensions`** part of the `AgentCapabilities` object.
+
+### Example AgentCard Declaration
+
+The callee agent uses the `supportedStateKeys` array to explicitly declare which contextual data keys it understands and is optimized to use.
+
+```json
+{
+ "uri": "https://github.com/a2aproject/a2a-samples/tree/main/samples/python/extensions/secure-passport",
+ "params": {
+ "supportedStateKeys": ["user_preferred_currency", "loyalty_tier"]
+ }
+}
+```
+
+## 3. Data Structure: CallerContext Payload
+
+The `callerContext` object is the Secure Passport payload. It is **optional** and is included in the `metadata` map of a core A2A message structure.
+
+| Field | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **`clientId`** | `string` | Yes | The verifiable unique identifier of the calling agent. |
+| **`signature`** | `string` | No | A digital signature of the entire `state` object, signed by the calling agent's private key, used for cryptographic verification of trust. |
+| **`sessionId`** | `string` | No | A session or conversation identifier to maintain thread continuity. |
+| **`state`** | `object` | Yes | A free-form JSON object containing the contextual data (e.g., user preferences, loyalty tier). |
+
+### Example CallerContext Payload
+
+```json
+{
+ "clientId": "a2a://orchestrator-agent.com",
+ "sessionId": "travel-session-xyz",
+ "signature": "MOCK-SIG-123456...",
+ "state": {
+ "user_preferred_currency": "GBP",
+ "loyalty_tier": "Gold"
+ }
+}
+```
+
+## 4. Message Augmentation and Example Usage
+
+The `CallerContext` payload is embedded directly into the `metadata` map of the A2A `Message` object. The key used **MUST** be the extension's URI: `https://github.com/a2aproject/a2a-samples/tree/main/samples/python/extensions/secure-passport`.
+
+### Example A2A Message Request (Simplified)
+
+This example shows the request body for an A2A `tasks/send` RPC call.
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": "req-123",
+ "method": "tasks/send",
+ "params": {
+ "message": {
+ "messageId": "msg-456",
+ "role": "user",
+ "parts": [
+ {"kind": "text", "content": "Book a flight for me."}
+ ],
+ "metadata": {
+ "https://github.com/a2aproject/a2a-samples/tree/main/samples/python/extensions/secure-passport": {
+ "clientId": "a2a://orchestrator-agent.com",
+ "sessionId": "travel-session-xyz",
+ "signature": "MOCK-SIG-123456...",
+ "state": {
+ "user_preferred_currency": "GBP",
+ "loyalty_tier": "Gold"
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+## 5. Implementation Notes and Best Practices
+
+This section addresses the use of SDK helpers and conceptual implementation patterns.
+
+### SDK Helper Methods
+
+For development efficiency, A2A SDKs **SHOULD** provide convenience methods for this extension, such as:
+
+* **AgentCard Utility:** A method to automatically generate the necessary JSON structure for the AgentCard declaration.
+* **Attachment/Extraction:** Simple functions or methods to add (`add_secure_passport`) and retrieve (`get_secure_passport`) the payload from a message object.
+
+### Conceptual Middleware Layer
+
+The most robust integration for the Secure Passport involves a **middleware layer** in the A2A SDK:
+
+* **Client Middleware:** Executes immediately before transport, automatically **attaching** the signed `CallerContext` to the message metadata.
+* **Server Middleware:** Executes immediately upon receiving the message, **extracting** the `CallerContext`, performing the cryptographic verification, and injecting the resulting context object into the client's execution environment.
+
+### Security and Callee Behavior
+
+1. **Verification:** A callee agent **SHOULD** verify the provided **`signature`** before relying on the `state` content for high-privilege actions.
+2. **Sensitive Data:** Agents **MUST NOT** include sensitive or mutable data in the `state` object unless robust, end-to-end cryptographic verification is implemented and required by the callee.
From d01ac65352488565125994839e300cb97a35920a Mon Sep 17 00:00:00 2001
From: Holt Skinner <13262395+holtskinner@users.noreply.github.com>
Date: Tue, 7 Oct 2025 14:01:47 -0500
Subject: [PATCH 07/14] ci: Remove Biome from Super Linter (#382)
---
.github/workflows/linter.yaml | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml
index 06d7c5a8..379922ec 100644
--- a/.github/workflows/linter.yaml
+++ b/.github/workflows/linter.yaml
@@ -5,17 +5,21 @@ on:
branches: [main]
jobs:
- build:
+ lint:
name: Lint Code Base
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: read
+ statuses: write
steps:
- name: Checkout Code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
fetch-depth: 0
- - name: Lint Code Base
+ - name: GitHub Super Linter
uses: super-linter/super-linter/slim@v8
env:
DEFAULT_BRANCH: main
@@ -44,3 +48,5 @@ jobs:
VALIDATE_JUPYTER_NBQA_PYLINT: false
VALIDATE_JUPYTER_NBQA_RUFF: false
VALIDATE_TRIVY: false
+ VALIDATE_BIOME_FORMAT: false
+ VALIDATE_BIOME_LINT: false
From a1a80ebabc2229c81d3f38b3b4a0e3909e33e09d Mon Sep 17 00:00:00 2001
From: gulliantonio <167304324+gulliantonio@users.noreply.github.com>
Date: Wed, 8 Oct 2025 17:27:03 +0200
Subject: [PATCH 08/14] feat: Implement AGP (Agent Gateway Protocol) for
policy-based routing (#381)
This PR introduces the Agent Gateway Protocol (AGP) as an architectural
enhancement layer designed to solve scalability and policy-enforcement
challenges in large, distributed A2A systems.
The AGP shifts routing from a flat mesh to a hierarchical model, using
central Gateway Agents to manage specialized Autonomous Squads (ASqs).
Key Innovation: Policy-Based Routing (PBR)
The core code implements PBR logic, verifying that routing decisions are
driven by compliance before cost, as verified by the unit tests:
1. Policy Enforcement: The Gateway successfully routes Intents based on
strict policy constraints (requires_PII: True), filtering out cheaper,
non-compliant vendor routes in favor of secure internal squads.
2. Economic Optimization: When multiple routes are compliant, the
Gateway correctly selects the option with the lowest announced cost.
3. Meta-Intent Coverage: The tests cover complex decomposition (e.g.,
breaking a single HR request into Payroll, Hardware, and Legal
sub-intents).
---
extensions/agp/agp_run.py | 124 ++++++
extensions/agp/poetry.lock | 391 ++++++++++++++++
extensions/agp/pyproject.toml | 25 ++
extensions/agp/spec.md | 112 +++++
extensions/agp/src/agp_protocol/__init__.py | 170 +++++++
extensions/agp/tests/test_agp.py | 470 ++++++++++++++++++++
6 files changed, 1292 insertions(+)
create mode 100644 extensions/agp/agp_run.py
create mode 100644 extensions/agp/poetry.lock
create mode 100644 extensions/agp/pyproject.toml
create mode 100644 extensions/agp/spec.md
create mode 100644 extensions/agp/src/agp_protocol/__init__.py
create mode 100644 extensions/agp/tests/test_agp.py
diff --git a/extensions/agp/agp_run.py b/extensions/agp/agp_run.py
new file mode 100644
index 00000000..e10b50f3
--- /dev/null
+++ b/extensions/agp/agp_run.py
@@ -0,0 +1,124 @@
+import logging
+
+from agp_protocol import (
+ AGPTable,
+ AgentGatewayProtocol,
+ CapabilityAnnouncement,
+ IntentPayload,
+)
+
+
+# Set logging level to WARNING so only our custom routing failures are visible
+logging.basicConfig(level=logging.WARNING)
+
+
+def run_simulation():
+ """Simulates the core routing process of the Agent Gateway Protocol (AGP),
+ demonstrating Policy-Based Routing and cost optimization.
+ """
+ # --- PHASE 1: Setup and Announcement ---
+
+ # 1. Initialize the central routing table
+ corporate_agp_table = AGPTable()
+
+ # 2. Initialize the Corporate Gateway Agent (Router)
+ corporate_gateway = AgentGatewayProtocol(
+ squad_name='Corporate_GW', agp_table=corporate_agp_table
+ )
+
+ # 3. Squads announce their capabilities to the Corporate Gateway
+
+ print('===============================================================')
+ print(' AGENT GATEWAY PROTOCOL (AGP) ROUTING SIMULATION')
+ print('===============================================================')
+ print('\n--- PHASE 1: SQUAD ANNOUNCEMENTS ---')
+
+ # --- Announcement 1: Engineering Squad (Internal, Secure) ---
+ # Can provision VMs, handles sensitive data (PII), but is more expensive than the external vendor.
+ eng_announcement = CapabilityAnnouncement(
+ capability='infra:provision:vm',
+ version='1.0',
+ cost=0.10, # Higher cost
+ policy={'security_level': 5, 'requires_PII': True},
+ )
+ corporate_gateway.announce_capability(
+ eng_announcement, path='Squad_Engineering/vm_provisioner'
+ )
+
+ # --- Announcement 2: External Vendor Squad (Cheapest, Low Security) ---
+ # Can provision VMs, but fails the PII check and only meets standard security.
+ vendor_announcement = CapabilityAnnouncement(
+ capability='infra:provision:vm',
+ version='1.1',
+ cost=0.05, # Lowest cost
+ policy={'security_level': 3, 'requires_PII': False},
+ )
+ corporate_gateway.announce_capability(
+ vendor_announcement, path='External_Vendor/vm_provisioning_api'
+ )
+
+ # --- Announcement 3: Finance Squad (Standard Analysis) ---
+ finance_announcement = CapabilityAnnouncement(
+ capability='financial_analysis:quarterly',
+ version='2.0',
+ cost=0.15,
+ policy={'security_level': 3, 'geo': 'US'},
+ )
+ corporate_gateway.announce_capability(
+ finance_announcement, path='Squad_Finance/analysis_tool'
+ )
+
+ # --- PHASE 2: Intent Routing Simulation ---
+
+ print('\n--- PHASE 2: INTENT ROUTING ---')
+
+ # Intent A: Standard VM provisioning (Cost-driven, minimal policy)
+ # Expected: Route to External Vendor (Cost: 0.05) because it's cheapest and complies with security_level: 3.
+ intent_a = IntentPayload(
+ target_capability='infra:provision:vm',
+ payload={'type': 'standard', 'user': 'bob'},
+ policy_constraints={'security_level': 3},
+ )
+ print(
+ '\n[Intent A] Requesting standard VM provisioning (Lowest cost, Security Level 3).'
+ )
+ corporate_gateway.route_intent(intent_a)
+
+ # Intent B: Sensitive VM provisioning (Policy-driven, requires PII)
+ # Expected: Route to Engineering Squad (Cost: 0.10) because the External Vendor (0.05) fails the PII policy.
+ # The router uses the sufficiency check (5 >= 5 is True).
+ intent_b = IntentPayload(
+ target_capability='infra:provision:vm',
+ payload={'type': 'sensitive', 'user': 'alice', 'data': 'ssn_data'},
+ policy_constraints={'security_level': 5, 'requires_PII': True},
+ )
+ print(
+ '\n[Intent B] Requesting sensitive VM provisioning (Requires PII and Security Level 5).'
+ )
+ corporate_gateway.route_intent(intent_b)
+
+ # Intent C: Requesting provisioning with security level 7 (Unmatched Policy)
+ # Expected: Fails because no announced route can satisfy level 7.
+ intent_c = IntentPayload(
+ target_capability='infra:provision:vm',
+ payload={'type': 'max_security'},
+ policy_constraints={'security_level': 7},
+ )
+ print(
+ '\n[Intent C] Requesting provisioning with security level 7 (Unmatched Policy).'
+ )
+ corporate_gateway.route_intent(intent_c)
+
+ # Intent D: Requesting HR onboarding (Unknown Capability)
+ # Expected: Fails because the capability was never announced.
+ intent_d = IntentPayload(
+ target_capability='hr:onboard:new_hire',
+ payload={'employee': 'Charlie'},
+ policy_constraints={},
+ )
+ print('\n[Intent D] Requesting HR onboarding (Unknown Capability).')
+ corporate_gateway.route_intent(intent_d)
+
+
+if __name__ == '__main__':
+ run_simulation()
diff --git a/extensions/agp/poetry.lock b/extensions/agp/poetry.lock
new file mode 100644
index 00000000..9a0ccb67
--- /dev/null
+++ b/extensions/agp/poetry.lock
@@ -0,0 +1,391 @@
+# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+description = "Reusable constraint types to use with typing.Annotated"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
+ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
+]
+
+[[package]]
+name = "autoflake"
+version = "2.3.1"
+description = "Removes unused imports and unused variables"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "autoflake-2.3.1-py3-none-any.whl", hash = "sha256:3ae7495db9084b7b32818b4140e6dc4fc280b712fb414f5b8fe57b0a8e85a840"},
+ {file = "autoflake-2.3.1.tar.gz", hash = "sha256:c98b75dc5b0a86459c4f01a1d32ac7eb4338ec4317a4469515ff1e687ecd909e"},
+]
+
+[package.dependencies]
+pyflakes = ">=3.0.0"
+tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+groups = ["dev"]
+markers = "sys_platform == \"win32\""
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.0"
+description = "Backport of PEP 654 (exception groups)"
+optional = false
+python-versions = ">=3.7"
+groups = ["dev"]
+markers = "python_version < \"3.11\""
+files = [
+ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"},
+ {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"},
+]
+
+[package.dependencies]
+typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""}
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "iniconfig"
+version = "2.1.0"
+description = "brain-dead simple config-ini parsing"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
+ {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
+ {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+description = "plugin and hook calling mechanisms for python"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
+ {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
+]
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["coverage", "pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pydantic"
+version = "2.11.9"
+description = "Data validation using Python type hints"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2"},
+ {file = "pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2"},
+]
+
+[package.dependencies]
+annotated-types = ">=0.6.0"
+pydantic-core = "2.33.2"
+typing-extensions = ">=4.12.2"
+typing-inspection = ">=0.4.0"
+
+[package.extras]
+email = ["email-validator (>=2.0.0)"]
+timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""]
+
+[[package]]
+name = "pydantic-core"
+version = "2.33.2"
+description = "Core functionality for Pydantic validation and serialization"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"},
+ {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"},
+ {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"},
+ {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"},
+ {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"},
+ {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"},
+ {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"},
+ {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"},
+ {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"},
+ {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"},
+ {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"},
+ {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"},
+ {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"},
+ {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"},
+ {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"},
+ {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"},
+ {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"},
+ {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"},
+ {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"},
+ {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"},
+ {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"},
+ {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"},
+ {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"},
+ {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"},
+ {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"},
+ {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"},
+ {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"},
+ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"},
+ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"},
+ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"},
+ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"},
+ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"},
+ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"},
+ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"},
+ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"},
+ {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"},
+ {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
+
+[[package]]
+name = "pyflakes"
+version = "3.4.0"
+description = "passive checker of Python programs"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"},
+ {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"},
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+description = "Pygments is a syntax highlighting package written in Python."
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+files = [
+ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
+ {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
+]
+
+[package.extras]
+windows-terminal = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "pytest"
+version = "8.4.2"
+description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"},
+ {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"},
+]
+
+[package.dependencies]
+colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""}
+iniconfig = ">=1"
+packaging = ">=20"
+pluggy = ">=1.5,<2"
+pygments = ">=2.7.2"
+tomli = {version = ">=1", markers = "python_version < \"3.11\""}
+
+[package.extras]
+dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
+
+[[package]]
+name = "ruff"
+version = "0.13.3"
+description = "An extremely fast Python linter and code formatter, written in Rust."
+optional = false
+python-versions = ">=3.7"
+groups = ["dev"]
+files = [
+ {file = "ruff-0.13.3-py3-none-linux_armv6l.whl", hash = "sha256:311860a4c5e19189c89d035638f500c1e191d283d0cc2f1600c8c80d6dcd430c"},
+ {file = "ruff-0.13.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2bdad6512fb666b40fcadb65e33add2b040fc18a24997d2e47fee7d66f7fcae2"},
+ {file = "ruff-0.13.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fc6fa4637284708d6ed4e5e970d52fc3b76a557d7b4e85a53013d9d201d93286"},
+ {file = "ruff-0.13.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c9e6469864f94a98f412f20ea143d547e4c652f45e44f369d7b74ee78185838"},
+ {file = "ruff-0.13.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5bf62b705f319476c78891e0e97e965b21db468b3c999086de8ffb0d40fd2822"},
+ {file = "ruff-0.13.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cc1abed87ce40cb07ee0667ce99dbc766c9f519eabfd948ed87295d8737c60"},
+ {file = "ruff-0.13.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4fb75e7c402d504f7a9a259e0442b96403fa4a7310ffe3588d11d7e170d2b1e3"},
+ {file = "ruff-0.13.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b951f9d9afb39330b2bdd2dd144ce1c1335881c277837ac1b50bfd99985ed3"},
+ {file = "ruff-0.13.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6052f8088728898e0a449f0dde8fafc7ed47e4d878168b211977e3e7e854f662"},
+ {file = "ruff-0.13.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc742c50f4ba72ce2a3be362bd359aef7d0d302bf7637a6f942eaa763bd292af"},
+ {file = "ruff-0.13.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8e5640349493b378431637019366bbd73c927e515c9c1babfea3e932f5e68e1d"},
+ {file = "ruff-0.13.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6b139f638a80eae7073c691a5dd8d581e0ba319540be97c343d60fb12949c8d0"},
+ {file = "ruff-0.13.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6b547def0a40054825de7cfa341039ebdfa51f3d4bfa6a0772940ed351d2746c"},
+ {file = "ruff-0.13.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9cc48a3564423915c93573f1981d57d101e617839bef38504f85f3677b3a0a3e"},
+ {file = "ruff-0.13.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1a993b17ec03719c502881cb2d5f91771e8742f2ca6de740034433a97c561989"},
+ {file = "ruff-0.13.3-py3-none-win32.whl", hash = "sha256:f14e0d1fe6460f07814d03c6e32e815bff411505178a1f539a38f6097d3e8ee3"},
+ {file = "ruff-0.13.3-py3-none-win_amd64.whl", hash = "sha256:621e2e5812b691d4f244638d693e640f188bacbb9bc793ddd46837cea0503dd2"},
+ {file = "ruff-0.13.3-py3-none-win_arm64.whl", hash = "sha256:9e9e9d699841eaf4c2c798fa783df2fabc680b72059a02ca0ed81c460bc58330"},
+ {file = "ruff-0.13.3.tar.gz", hash = "sha256:5b0ba0db740eefdfbcce4299f49e9eaefc643d4d007749d77d047c2bab19908e"},
+]
+
+[[package]]
+name = "tomli"
+version = "2.2.1"
+description = "A lil' TOML parser"
+optional = false
+python-versions = ">=3.8"
+groups = ["dev"]
+markers = "python_version < \"3.11\""
+files = [
+ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
+ {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
+ {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
+ {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
+ {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
+ {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
+ {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
+ {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
+ {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
+ {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
+ {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
+ {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
+ {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
+ {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+description = "Backported and Experimental Type Hints for Python 3.9+"
+optional = false
+python-versions = ">=3.9"
+groups = ["main", "dev"]
+files = [
+ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
+ {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
+]
+markers = {dev = "python_version < \"3.11\""}
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+description = "Runtime typing introspection tools"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"},
+ {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.12.0"
+
+[metadata]
+lock-version = "2.1"
+python-versions = "^3.9"
+content-hash = "afa56f22f84c1320786f16aa43157a3fbea2db221eb6e09e88de60a4f5a5ec0a"
diff --git a/extensions/agp/pyproject.toml b/extensions/agp/pyproject.toml
new file mode 100644
index 00000000..c6497659
--- /dev/null
+++ b/extensions/agp/pyproject.toml
@@ -0,0 +1,25 @@
+[tool.poetry]
+name = "agp-protocol"
+version = "1.0.0"
+description = "Agent Gateway Protocol (AGP) routing layer implementation."
+authors = ["Google Octo "]
+license = "Apache-2.0"
+
+# Defines where the source code package 'agp_protocol' is located (inside the 'src' folder)
+packages = [
+ { include = "agp_protocol", from = "src" }
+]
+
+[tool.poetry.dependencies]
+python = "^3.9"
+pydantic = "^2.0.0"
+
+[tool.poetry.group.dev.dependencies]
+# Dependencies needed only for development and testing
+pytest = "^8.0.0"
+ruff = "^0.13.3"
+autoflake = "^2.3.1"
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/extensions/agp/spec.md b/extensions/agp/spec.md
new file mode 100644
index 00000000..6ad88ce1
--- /dev/null
+++ b/extensions/agp/spec.md
@@ -0,0 +1,112 @@
+# Agent Gateway Protocol (AGP) Specification (V1)
+
+* **URI:** `https://github.com/a2aproject/a2a-samples/tree/main/extensions/agp`
+
+* **Type:** Core Protocol Layer / Routing Extension
+
+* **Version:** 1.0.0
+
+## Abstract
+
+The Agent Gateway Protocol (AGP) proposes a hierarchical architecture for distributed AI systems, **enhancing the capabilities** of the flat A2A mesh by introducing a structure of interconnected Autonomous Squads (ASq). AGP routes **Intent** payloads based on declared **Capabilities**, mirroring the Border Gateway Protocol (BGP) for Internet scalability and policy enforcement. This structure divides agents into **hierarchical domains**, with each domain focusing on specific Agent Capabilities that reflect enterprise organizational needs (e.g., Finance, Engineering, HR, BUs, and so on) - gulli@google.com
+
+## 1. Data Structure: Capability Announcement
+
+This payload is used by a Squad Gateway Agent to announce the services its squad can fulfill to its peers (other squads).
+
+### CapabilityAnnouncement Object Schema
+
+| Field | Type | Required | Description |
+ | ----- | ----- | ----- | ----- |
+| `capability` | string | Yes | The function or skill provided (e.g., `financial_analysis:quarterly`). |
+| `version` | string | Yes | Version of the capability schema/interface (e.g., `1.5`). |
+| `cost` | number | No | Estimated cost metric (e.g., `0.05` USD, or token count). |
+| `policy` | object | Yes | Key-value pairs defining required policies (e.g., `requires_pii:true`, `security_level:5`). |
+
+### Example Announcement Payload
+
+```json
+{
+ "capability": "financial_analysis:quarterly",
+ "version": "1.5",
+ "cost": 0.05,
+ "policy": {
+ "requires_auth": "level_3"
+ }
+}
+```
+
+## 2. Data Structure: Intent Payload
+
+This payload defines the *what* (the goal) and *constraints* (metadata), replacing a standard request.
+
+### Intent Object Schema
+
+| Field | Type | Required | Description |
+ | ----- | ----- | ----- | ----- |
+| `target_capability` | string | Yes | The capability the Intent seeks to fulfill. |
+| `payload` | object | Yes | The core data arguments required for the task. |
+| `policy_constraints` | object | No | Client-defined constraints that must be matched against the announced `policy` during routing. |
+
+### Example Intent Payload
+
+```json
+{
+ "target_capability": "billing:invoice:generate",
+ "payload": {
+ "customer_id": 123,
+ "amount": 99.99
+ },
+ "policy_constraints": {
+ "requires_pii": true
+ }
+}
+```
+
+## 3. Core Routing and Table Structures
+
+The protocol relies on the Gateway Agent maintaining an **AGP Table** (a routing table) built from Capability Announcements. This section defines the core structures used internally by the Gateway Agent.
+
+### A. RouteEntry Object Schema
+
+| Field | Type | Required | Description |
+ | ----- | ----- | ----- | ----- |
+| `path` | string | Yes | The destination Squad/API path (e.g., `Squad_Finance/gateway`). |
+| `cost` | number | Yes | The cost metric for this route (used for lowest-cost selection). |
+| `policy` | object | Yes | Policies of the destination, used for matching Intent constraints. |
+
+### B. AGPTable Object
+
+The AGPTable maps a `capability` key to a list of potential `RouteEntry` objects.
+
+## 4. Agent Declaration and Role
+
+To participate in the AGP hierarchy, an A2A agent **MUST** declare its role as a Gateway and the supported AGP version within its Agent Card, using the A2A extension mechanism.
+
+### AgentCard AGP Declaration
+
+This declaration is placed within the `extensions` array of the Agent Card's `AgentCapabilities`.
+
+```json
+{
+ "uri": "https://github.com/a2aproject/a2a-samples/tree/main/extensions/agp",
+ "params": {
+ "agent_role": "gateway",
+ "supported_agp_versions": ["1.0"]
+ }
+}
+```
+
+## 5. Extension Error Reference
+
+When a Gateway Agent attempts to route an Intent but fails due to policy or availability issues, it **MUST** return a JSON-RPC error with specific AGP-defined codes.
+
+| Code | Name | Description | Routing Consequence |
+ | ----- | ----- | ----- | ----- |
+| **-32200** | `AGP_ROUTE_NOT_FOUND` | No agent or squad has announced the requested `target_capability`. | Intent cannot be routed; returned to sender. |
+| **-32201** | `AGP_POLICY_VIOLATION` | Routes were found, but none satisfied the constraints in the Intent's `metadata` (e.g., no squad accepts PII data). | Intent cannot be routed safely; returned to sender. |
+| **-32202** | `AGP_TABLE_STALE` | The Agent Gateway's routing table is outdated and needs a refresh via a standard AGP refresh mechanism. | Gateway attempts refresh before re-routing, or returns error. |
+
+## 6. Conclusion
+
+The Agent Gateway Protocol (AGP) offers a powerful and necessary enhancement layer over the foundational A2A structure. By implementing Policy-Based Routing, AGP ensures that distributed AI systems are not only efficient and financially optimized but also secure and policy-compliant—a critical step toward trustworthy, industrial-scale multi-agent collaboration.
diff --git a/extensions/agp/src/agp_protocol/__init__.py b/extensions/agp/src/agp_protocol/__init__.py
new file mode 100644
index 00000000..84f6b3d6
--- /dev/null
+++ b/extensions/agp/src/agp_protocol/__init__.py
@@ -0,0 +1,170 @@
+import logging
+
+from typing import Any, Optional
+
+from pydantic import BaseModel, ConfigDict, Field
+
+
+# --- Core Data Structures ---
+
+
+class CapabilityAnnouncement(BaseModel):
+ """Data structure for a service announcement by a Gateway Agent."""
+
+ capability: str = Field(
+ ...,
+ description="The function or skill provided (e.g., 'financial_analysis:quarterly').",
+ )
+ version: str = Field(..., description='Version of the capability schema.')
+ cost: float | None = Field(None, description='Estimated cost metric.')
+ policy: dict[str, Any] = Field(
+ ...,
+ description='Key-value pairs defining required security/data policies.',
+ )
+
+ model_config = ConfigDict(extra='forbid')
+
+
+class IntentPayload(BaseModel):
+ """The request payload routed by AGP."""
+
+ target_capability: str = Field(
+ ..., description='The capability the Intent seeks to fulfill.'
+ )
+ payload: dict[str, Any] = Field(
+ ..., description='The core data arguments required for the task.'
+ )
+ # FIX APPLIED: Renaming internal field to policy_constraints for clarity
+ policy_constraints: dict[str, Any] = Field(
+ default_factory=dict,
+ description='Client-defined constraints that must be matched against the announced policy.',
+ alias='policy_constraints',
+ )
+
+ model_config = ConfigDict(extra='forbid', populate_by_name=True)
+
+
+# --- AGP Routing Structures ---
+
+
+class RouteEntry(BaseModel):
+ """A single possible route to fulfill a fulfill a capability."""
+
+ path: str = Field(
+ ...,
+ description="The destination Squad/API path (e.g., 'Squad_Finance/gateway').",
+ )
+ cost: float = Field(..., description='Cost metric for this route.')
+ policy: dict[str, Any] = Field(
+ ...,
+ description='Policies of the destination, used for matching Intent constraints.',
+ )
+
+
+class AGPTable(BaseModel):
+ """The central routing table maintained by a Gateway Agent."""
+
+ routes: dict[str, list[RouteEntry]] = Field(default_factory=dict)
+
+ model_config = ConfigDict(extra='forbid')
+
+
+# --- Core AGP Routing Logic ---
+
+
+class AgentGatewayProtocol:
+ """
+ Simulates the core functions of an Autonomous Squad Gateway Agent.
+ Handles Capability Announcements and Policy-Based Intent Routing.
+ The primary routing logic is in _select_best_route to allow easy overriding via subclassing.
+ """
+
+ def __init__(self, squad_name: str, agp_table: AGPTable):
+ self.squad_name = squad_name
+ self.agp_table = agp_table
+
+ def announce_capability(
+ self, announcement: CapabilityAnnouncement, path: str
+ ):
+ """Simulates receiving a capability announcement and updating the AGP Table."""
+ entry = RouteEntry(
+ path=path,
+ cost=announcement.cost or 0.0,
+ policy=announcement.policy,
+ )
+
+ capability_key = announcement.capability
+
+ # Use setdefault to initialize the list if the key is new
+ self.agp_table.routes.setdefault(capability_key, []).append(entry)
+
+ print(
+ f'[{self.squad_name}] ANNOUNCED: {capability_key} routed via {path}'
+ )
+
+ # Protected method containing the core, overridable routing logic
+ def _select_best_route(self, intent: IntentPayload) -> RouteEntry | None:
+ """
+ Performs Policy-Based Routing to find the best available squad.
+
+ Routing Logic:
+ 1. Find all routes matching the target_capability.
+ 2. Filter routes based on matching all policy constraints (PBR).
+ 3. Select the lowest-cost route among the compliant options.
+ """
+ target_cap = intent.target_capability
+ # CRITICAL CHANGE: Use the correct snake_case attribute name for constraints
+ intent_constraints = intent.policy_constraints
+
+ if target_cap not in self.agp_table.routes:
+ logging.warning(
+ f"[{self.squad_name}] ROUTING FAILED: Capability '{target_cap}' is unknown."
+ )
+ return None
+
+ possible_routes = self.agp_table.routes[target_cap]
+
+ # --- 2. Policy Filtering (Optimized using list comprehension and all()) ---
+ compliant_routes = [
+ route
+ for route in possible_routes
+ if all(
+ # Check if the constraint key exists in the route policy AND the values are sufficient.
+ key in route.policy
+ and (
+ # If the key is 'security_level' and both values are numeric, check for >= sufficiency.
+ route.policy[key] >= value
+ if key == 'security_level'
+ and isinstance(route.policy.get(key), (int, float))
+ and isinstance(value, (int, float))
+ # Otherwise (e.g., boolean flags like 'requires_PII'), require exact equality.
+ else route.policy[key] == value
+ )
+ for key, value in intent_constraints.items()
+ )
+ ]
+
+ if not compliant_routes:
+ logging.warning(
+ f'[{self.squad_name}] ROUTING FAILED: No compliant route found for constraints: {intent_constraints}'
+ )
+ return None
+
+ # --- 3. Best Route Selection (Lowest Cost) ---
+ best_route = min(compliant_routes, key=lambda r: r.cost)
+
+ return best_route
+
+ # Public method that is typically called by the A2A endpoint
+ def route_intent(self, intent: IntentPayload) -> RouteEntry | None:
+ """
+ Public entry point for routing an Intent payload.
+ Calls the internal selection logic and prints the result.
+ """
+ best_route = self._select_best_route(intent)
+
+ if best_route:
+ print(
+ f"[{self.squad_name}] ROUTING SUCCESS: Intent for '{intent.target_capability}' routed to {best_route.path} (Cost: {best_route.cost})"
+ )
+ return best_route
diff --git a/extensions/agp/tests/test_agp.py b/extensions/agp/tests/test_agp.py
new file mode 100644
index 00000000..083986a6
--- /dev/null
+++ b/extensions/agp/tests/test_agp.py
@@ -0,0 +1,470 @@
+import pytest
+
+from agp_protocol import (
+ AGPTable,
+ AgentGatewayProtocol,
+ CapabilityAnnouncement,
+ IntentPayload,
+ RouteEntry,
+)
+
+
+# --- Fixtures for Routing Table Setup ---
+
+
+@pytest.fixture
+def all_available_routes() -> list[RouteEntry]:
+ """Defines a list of heterogeneous routes covering all capabilities needed for testing."""
+ return [
+ # 1. Base License/Legal Route (Security Level 3, Geo US) - Cost 0.20
+ RouteEntry(
+ path='Squad_Legal/licensing_api',
+ cost=0.20,
+ policy={'security_level': 3, 'geo': 'US'},
+ ),
+ # 2. Secure/PII Route (Security Level 5, PII Handling True, Geo US) - Cost 0.10
+ RouteEntry(
+ path='Squad_Finance/payroll_service',
+ cost=0.10,
+ policy={'security_level': 5, 'requires_pii': True, 'geo': 'US'},
+ ),
+ # 3. External Route (Cheapest, Low Security, Geo EU) - Cost 0.05
+ RouteEntry(
+ path='Vendor_EU/proxy_gateway',
+ cost=0.05,
+ policy={'security_level': 1, 'geo': 'EU'},
+ ),
+ # 4. Hardware Provisioning Route (Engineering, Security Level 3, Geo US) - Cost 0.08
+ RouteEntry(
+ path='Squad_Engineering/hardware_tool',
+ cost=0.08,
+ policy={'security_level': 3, 'geo': 'US'},
+ ),
+ # 5. NDA Contract Generation Route (Legal, Security Level 3, Geo US) - Cost 0.15
+ RouteEntry(
+ path='Squad_Legal/contracts_tool',
+ cost=0.15,
+ policy={'security_level': 3, 'geo': 'US'},
+ ),
+ # 6. Low-Cost US Route (Security Level 2, Geo US) - Cost 0.07
+ RouteEntry(
+ path='Vendor_US/data_service',
+ cost=0.07,
+ policy={'security_level': 2, 'geo': 'US'},
+ ),
+ # 7. Zero-Cost Internal Route (Security Level 3, Geo US) - Cost 0.00 (NEW)
+ RouteEntry(
+ path='Internal/Free_Cache',
+ cost=0.00,
+ policy={'security_level': 3, 'geo': 'US'},
+ ),
+ # 8. High-Cost Geo EU Route (Security Level 4, Geo EU) - Cost 0.30 (NEW)
+ RouteEntry(
+ path='Vendor_Secure_EU/proxy_gateway',
+ cost=0.30,
+ policy={'security_level': 4, 'geo': 'EU'},
+ ),
+ ]
+
+
+@pytest.fixture
+def populated_agp_table(all_available_routes) -> AGPTable:
+ """Creates an AGPTable populated with routes for all test capabilities."""
+ table = AGPTable()
+
+ # Routes for Core Routing Tests (Tests 1-19 use 'procure:license')
+ table.routes['procure:license'] = [
+ all_available_routes[0],
+ all_available_routes[1],
+ all_available_routes[2],
+ all_available_routes[5],
+ all_available_routes[6], # Zero Cost Route
+ all_available_routes[7], # Secure EU Route
+ ]
+
+ # Routes for Decomposition Test (Test 6)
+ table.routes['provision:hardware'] = [all_available_routes[3]]
+ table.routes['provision:payroll'] = [all_available_routes[1]]
+ table.routes['contract:nda:generate'] = [all_available_routes[4]]
+
+ return table
+
+
+@pytest.fixture
+def gateway(populated_agp_table) -> AgentGatewayProtocol:
+ """Provides a configured Gateway Agent instance for testing."""
+ return AgentGatewayProtocol(
+ squad_name='Test_Gateway', agp_table=populated_agp_table
+ )
+
+
+# --- Test Scenarios (19 Total Tests) ---
+
+
+def test_01_lowest_cost_compliant_route_with_sufficiency(
+ gateway: AgentGatewayProtocol,
+):
+ """
+ Verifies routing selects the lowest cost COMPLIANT route, checking for sufficiency (>=).
+ Constraint: security_level: 3, geo: US. Route 7 (Cost 0.00) is the cheapest compliant route.
+ Expected: Route 7 (Internal/Free_Cache, Cost 0.00).
+ """
+ intent = IntentPayload(
+ target_capability='procure:license',
+ payload={'item': 'Standard License'},
+ policy_constraints={'security_level': 3, 'geo': 'US'},
+ )
+
+ best_route = gateway.route_intent(intent)
+
+ assert best_route is not None
+ assert best_route.path == 'Internal/Free_Cache'
+ assert best_route.cost == 0.00
+
+
+def test_02_policy_filtering_sensitive_data(gateway: AgentGatewayProtocol):
+ """
+ Verifies strict policy filtering excludes non-compliant routes regardless of cost.
+ Constraint: requires_pii: True. Only Route 2 complies (Cost 0.10).
+ Expected: Route 2 (Squad_Finance/payroll_service, Cost 0.10).
+ """
+ intent = IntentPayload(
+ target_capability='procure:license',
+ payload={'item': 'Client Data License'},
+ policy_constraints={'requires_pii': True},
+ )
+
+ best_route = gateway.route_intent(intent)
+
+ assert best_route is not None
+ assert best_route.path == 'Squad_Finance/payroll_service'
+ assert best_route.cost == 0.10
+
+
+def test_03_route_not_found(gateway: AgentGatewayProtocol):
+ """Tests routing failure when the target capability is not in the AGPTable."""
+ intent = IntentPayload(
+ target_capability='unknown:capability', payload={'data': 'test'}
+ )
+ best_route = gateway.route_intent(intent)
+ assert best_route is None
+
+
+def test_04_policy_violation_unmatched_constraint(
+ gateway: AgentGatewayProtocol,
+):
+ """
+ Tests routing failure when the Intent imposes a constraint that no announced route can meet.
+ Constraint: security_level: 7. No route announces level 7 or higher.
+ """
+ intent = IntentPayload(
+ target_capability='procure:license',
+ payload={'item': 'Executive Access'},
+ policy_constraints={'security_level': 7},
+ )
+ best_route = gateway.route_intent(intent)
+ assert best_route is None
+
+
+def test_05_announcement_updates_table(gateway: AgentGatewayProtocol):
+ """Tests that announce_capability correctly adds a new entry to the AGPTable."""
+ announcement = CapabilityAnnouncement(
+ capability='test:add:new',
+ version='1.0',
+ cost=1.0,
+ policy={'test': True, 'security_level': 1},
+ )
+ path = 'TestSquad/target'
+
+ # Check table before announcement
+ assert 'test:add:new' not in gateway.agp_table.routes
+
+ gateway.announce_capability(announcement, path)
+
+ # Check table after announcement
+ assert 'test:add:new' in gateway.agp_table.routes
+ assert len(gateway.agp_table.routes['test:add:new']) == 1
+ assert gateway.agp_table.routes['test:add:new'][0].path == path
+
+
+def test_06_meta_intent_decomposition(gateway: AgentGatewayProtocol):
+ """
+ Simulates the Corporate Enterprise flow: decomposition into three sub-intents
+ and verifies each sub-intent routes to the correct specialist squad based on policies.
+ """
+
+ # 1. Hardware Sub-Intent (Standard Engineering Task, requires level 3)
+ intent_hardware = IntentPayload(
+ target_capability='provision:hardware',
+ payload={'developer': 'Alice'},
+ policy_constraints={'security_level': 3},
+ )
+ route_hw = gateway.route_intent(intent_hardware)
+ assert route_hw is not None
+ assert route_hw.path == 'Squad_Engineering/hardware_tool'
+
+ # 2. Payroll Sub-Intent (Requires PII Handling - must go to secure Finance squad)
+ intent_payroll = IntentPayload(
+ target_capability='provision:payroll',
+ payload={'salary': 100000},
+ policy_constraints={'requires_pii': True, 'security_level': 3},
+ )
+ route_payroll = gateway.route_intent(intent_payroll)
+ assert route_payroll is not None
+ assert route_payroll.path == 'Squad_Finance/payroll_service'
+
+ # 3. Legal Sub-Intent (Simple route for contract:nda:generate, requires level 3)
+ intent_legal = IntentPayload(
+ target_capability='contract:nda:generate',
+ payload={'contract_type': 'NDA'},
+ policy_constraints={'security_level': 3},
+ )
+ route_legal = gateway.route_intent(intent_legal)
+ assert route_legal is not None
+ assert route_legal.path == 'Squad_Legal/contracts_tool'
+
+
+# --- NEW SECURITY AND COMPLIANCE TESTS ---
+
+
+def test_07_geo_fencing_violation(gateway: AgentGatewayProtocol):
+ """
+ Tests routing failure when an Intent requires US processing, but the cheapest route is EU-locked.
+ Constraint: geo: US. External Vendor (Cost 0.05, EU) fails geo-check.
+ Expected: Routed to cheapest compliant US vendor (Internal/Free_Cache, Cost 0.00).
+ """
+ intent = IntentPayload(
+ target_capability='procure:license',
+ payload={'data': 'US-user-request'},
+ policy_constraints={'geo': 'US'},
+ )
+
+ best_route = gateway.route_intent(intent)
+
+ assert best_route is not None
+ assert best_route.path == 'Internal/Free_Cache'
+ assert best_route.cost == 0.00
+
+
+def test_08_required_security_tier_sufficiency(gateway: AgentGatewayProtocol):
+ """
+ Tests routing when a request requires a moderate security level (4).
+ The router must choose Route 2 (Level 5) because Route 1 (Level 3) and Route 6 (Level 2) fail the sufficiency check.
+ Constraint: security_level: 4.
+ Expected: Route 2 (Squad_Finance/payroll_service, Cost 0.10).
+ """
+ intent = IntentPayload(
+ target_capability='procure:license',
+ payload={'data': 'moderate_access'},
+ policy_constraints={'security_level': 4},
+ )
+
+ best_route = gateway.route_intent(intent)
+
+ assert best_route is not None
+ assert best_route.path == 'Squad_Finance/payroll_service'
+ assert best_route.cost == 0.10
+
+
+def test_09_policy_chaining_cost_after_geo(gateway: AgentGatewayProtocol):
+ """
+ Tests routing for a complex chain: Intent requires US geo AND Level 2 security.
+ Compliant routes: Route 7 (0.00, L3), Route 6 (0.07, L2), Route 2 (0.10, L5), Route 1 (0.20, L3).
+ Expected: Cheapest compliant US route (Internal/Free_Cache, Cost 0.00).
+ """
+ intent = IntentPayload(
+ target_capability='procure:license',
+ payload={'request': 'simple_data_pull'},
+ policy_constraints={'security_level': 2, 'geo': 'US'},
+ )
+
+ best_route = gateway.route_intent(intent)
+
+ assert best_route is not None
+ assert best_route.path == 'Internal/Free_Cache'
+ assert best_route.cost == 0.00
+
+
+def test_10_zero_cost_priority(gateway: AgentGatewayProtocol):
+ """
+ Tests that the absolute cheapest route (Cost 0.00) is prioritized when compliant.
+ Constraint: security_level: 3, geo: US. Route 7 (Cost 0.00) meets the need.
+ Expected: Route 7 (Internal/Free_Cache, Cost 0.00).
+ """
+ intent = IntentPayload(
+ target_capability='procure:license',
+ payload={'request': 'cache_check'},
+ policy_constraints={'security_level': 3, 'geo': 'US'},
+ )
+
+ best_route = gateway.route_intent(intent)
+
+ assert best_route is not None
+ assert best_route.path == 'Internal/Free_Cache'
+ assert best_route.cost == 0.00
+
+
+def test_11_minimum_security_level_one_selection(gateway: AgentGatewayProtocol):
+ """
+ Tests routing for the absolute lowest security requirement.
+ Constraint: security_level: 1. Route 7 (Cost 0.00) is the cheapest compliant route.
+ Expected: Route 7 (Internal/Free_Cache, Cost 0.00).
+ """
+ # NOTE: All routes are compliant (L1, L3, L5, L2, L3, L3, L4). Cheapest is Route 7 (Cost 0.00).
+ intent = IntentPayload(
+ target_capability='procure:license',
+ payload={'request': 'public_data_access'},
+ policy_constraints={'security_level': 1},
+ )
+
+ best_route = gateway.route_intent(intent)
+
+ assert best_route is not None
+ assert best_route.path == 'Internal/Free_Cache'
+ assert best_route.cost == 0.00
+
+
+def test_12_strict_geo_exclusion(gateway: AgentGatewayProtocol):
+ """
+ Tests routing failure when requested geo (NA) is not available anywhere.
+ Constraint: geo: NA. No route advertises 'NA'.
+ Expected: Fails to route.
+ """
+ intent = IntentPayload(
+ target_capability='procure:license',
+ payload={'request': 'NA_access'},
+ policy_constraints={'geo': 'NA'},
+ )
+
+ best_route = gateway.route_intent(intent)
+
+ assert best_route is None
+
+
+def test_13_cost_tie_breaker(gateway: AgentGatewayProtocol):
+ """
+ Tests the tie-breaker mechanism when two compliant routes have the exact same cost.
+ Constraint: security_level: 5, geo: US. Only Route 2 (Cost 0.10, Level 5) is compliant.
+ Expected: Route 2 (Squad_Finance/payroll_service, Cost 0.10).
+ """
+ intent = IntentPayload(
+ target_capability='procure:license',
+ payload={'request': 'high_security_check'},
+ policy_constraints={'security_level': 5, 'geo': 'US'},
+ )
+
+ best_route = gateway.route_intent(intent)
+
+ assert best_route is not None
+ assert best_route.path == 'Squad_Finance/payroll_service'
+ assert best_route.cost == 0.10
+
+
+def test_14_no_constraint_default_cheapest(gateway: AgentGatewayProtocol):
+ """
+ Tests routing when the Intent provides no constraints (empty metadata).
+ Expected: Router must select the absolute cheapest route available (Route 7, Cost 0.00).
+ """
+ # NOTE: Route 7 (Cost 0.00) is the cheapest overall.
+ intent = IntentPayload(
+ target_capability='procure:license',
+ payload={'request': 'simple_unsecured'},
+ policy_constraints={},
+ )
+
+ best_route = gateway.route_intent(intent)
+
+ assert best_route is not None
+ assert best_route.path == 'Internal/Free_Cache'
+ assert best_route.cost == 0.00
+
+
+def test_15_compound_exclusion(gateway: AgentGatewayProtocol):
+ """
+ Tests routing failure when two mandatory constraints cannot be met by the same route.
+ Constraint: geo: EU AND security_level: 5.
+ Expected: Failure (Route 8 is EU but only L4; Route 2 is L5 but US).
+ """
+ intent = IntentPayload(
+ target_capability='procure:license',
+ payload={'request': 'EU_secure_data'},
+ policy_constraints={'geo': 'EU', 'security_level': 5},
+ )
+
+ best_route = gateway.route_intent(intent)
+
+ assert best_route is None
+
+
+def test_16_decomposition_check_pii_only_route(gateway: AgentGatewayProtocol):
+ """
+ Verifies that the decomposition test logic for Payroll correctly chooses the PII-handling route.
+ This is a redundant check to ensure Test 06's complexity is fully stable.
+ """
+ intent_payroll = IntentPayload(
+ target_capability='provision:payroll',
+ payload={'salary': 100000},
+ policy_constraints={'requires_pii': True, 'security_level': 3},
+ )
+ route_payroll = gateway.route_intent(intent_payroll)
+ assert route_payroll is not None
+ assert route_payroll.path == 'Squad_Finance/payroll_service'
+
+
+def test_17_cost_wins_after_sufficiency_filter(gateway: AgentGatewayProtocol):
+ """
+ Tests that after filtering for sufficiency (Level >= 2), the cheapest route is chosen.
+ Compliant routes: Route 7 (0.00, L3), Route 6 (0.07, L2), Route 2 (0.10, L5), Route 1 (0.20, L3).
+ Expected: Cheapest compliant route (Internal/Free_Cache, Cost 0.00).
+ """
+ intent = IntentPayload(
+ target_capability='procure:license',
+ payload={'request': 'simple_data_pull'},
+ policy_constraints={'security_level': 2},
+ )
+
+ best_route = gateway.route_intent(intent)
+
+ assert best_route is not None
+ assert best_route.path == 'Internal/Free_Cache'
+ assert best_route.cost == 0.00
+
+
+def test_18_sufficiency_check_for_level_1_route_wins(
+ gateway: AgentGatewayProtocol,
+):
+ """
+ Tests that a request for L1 security is satisfied by the cheapest overall route (L1, 0.05).
+ Constraint: security_level: 1.
+ Expected: Router must select the absolute cheapest route available (Route 7, Cost 0.00).
+ """
+ # NOTE: All routes are L1 or higher. Cheapest is Route 7 (Cost 0.00).
+ intent = IntentPayload(
+ target_capability='procure:license',
+ payload={'request': 'lowest_security'},
+ policy_constraints={'security_level': 1},
+ )
+
+ best_route = gateway.route_intent(intent)
+
+ assert best_route is not None
+ assert best_route.path == 'Internal/Free_Cache'
+ assert best_route.cost == 0.00
+
+
+def test_19_compound_geo_and_sufficiency_win(gateway: AgentGatewayProtocol):
+ """
+ Tests a chain of filters: Needs geo: US AND security_level: 5.
+ Expected: Route 2 (Cost 0.10) is the only one that meets both.
+ """
+ intent = IntentPayload(
+ target_capability='procure:license',
+ payload={'request': 'US_secure_finance'},
+ policy_constraints={'security_level': 5, 'geo': 'US'},
+ )
+
+ best_route = gateway.route_intent(intent)
+
+ assert best_route is not None
+ assert best_route.path == 'Squad_Finance/payroll_service'
+ assert best_route.cost == 0.10
From 834c1e7b843115d94722668b1ce27c0584dfda6e Mon Sep 17 00:00:00 2001
From: Holt Skinner <13262395+holtskinner@users.noreply.github.com>
Date: Wed, 15 Oct 2025 09:53:26 -0500
Subject: [PATCH 09/14] ci: Remove lint checks for Zizmor and AGP
---
.github/workflows/linter.yaml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml
index 379922ec..29099b9a 100644
--- a/.github/workflows/linter.yaml
+++ b/.github/workflows/linter.yaml
@@ -27,7 +27,7 @@ jobs:
LOG_LEVEL: WARN
SHELLCHECK_OPTS: -e SC1091 -e 2086
VALIDATE_ALL_CODEBASE: false
- FILTER_REGEX_EXCLUDE: "^(\\.github/|\\.vscode/).*"
+ FILTER_REGEX_EXCLUDE: "^(\\.github/|\\.vscode/).*|CODE_OF_CONDUCT.md|(extensions/agp/).*"
VALIDATE_PYTHON_BLACK: false
VALIDATE_PYTHON_FLAKE8: false
VALIDATE_PYTHON_ISORT: false
@@ -50,3 +50,4 @@ jobs:
VALIDATE_TRIVY: false
VALIDATE_BIOME_FORMAT: false
VALIDATE_BIOME_LINT: false
+ VALIDATE_GITHUB_ACTIONS_ZIZMOR: false
From 3b98b0f6eaad811b99a682c37445714ee8f7fc00 Mon Sep 17 00:00:00 2001
From: Farah Juma
Date: Fri, 24 Oct 2025 14:24:09 -0400
Subject: [PATCH 10/14] feat: Add a sample that shows how to configure A2A
client and A2A server security for all three transports with the A2A Java SDK
(#390)
# Description
This PR adds a new sample that demonstrates how to secure an A2A server
with Keycloak using bearer token authentication and it shows how to
configure an A2A client to specify the token when sending requests. The
agent is written using Quarkus LangChain4j and makes use of the [A2A
Java](https://github.com/a2aproject/a2a-java) SDK.
- [x] Follow the [`CONTRIBUTING`
Guide](https://github.com/a2aproject/a2a-samples/blob/main/CONTRIBUTING.md).
---
samples/java/agents/README.md | 3 +
samples/java/agents/content_editor/pom.xml | 45 +---
samples/java/agents/content_writer/pom.xml | 45 +---
.../samples/a2a/client/TestClientRunner.java | 6 +-
.../agents/dice_agent_multi_transport/pom.xml | 72 +-----
.../agents/magic_8_ball_security/README.md | 120 +++++++++
.../magic_8_ball_security/client/pom.xml | 74 ++++++
.../KeycloakOAuth2CredentialService.java | 55 +++++
.../com/samples/a2a/client/TestClient.java | 119 +++++++++
.../samples/a2a/client/TestClientRunner.java | 231 ++++++++++++++++++
.../com/samples/a2a/client/package-info.java | 2 +
.../samples/a2a/client/util/CachedToken.java | 91 +++++++
.../a2a/client/util/EventHandlerUtil.java | 118 +++++++++
.../samples/a2a/client/util/KeycloakUtil.java | 95 +++++++
.../samples/a2a/client/util/package-info.java | 2 +
.../client/src/main/resources/keycloak.json | 9 +
.../java/agents/magic_8_ball_security/pom.xml | 20 ++
.../magic_8_ball_security/server/.env.example | 1 +
.../magic_8_ball_security/server/pom.xml | 65 +++++
.../java/com/samples/a2a/Magic8BallAgent.java | 41 ++++
.../a2a/Magic8BallAgentCardProducer.java | 96 ++++++++
.../a2a/Magic8BallAgentExecutorProducer.java | 118 +++++++++
.../java/com/samples/a2a/Magic8BallTools.java | 59 +++++
.../java/com/samples/a2a/package-info.java | 2 +
.../src/main/resources/application.properties | 7 +
samples/java/agents/pom.xml | 87 +++++++
samples/java/agents/weather_mcp/pom.xml | 45 +---
27 files changed, 1442 insertions(+), 186 deletions(-)
create mode 100644 samples/java/agents/magic_8_ball_security/README.md
create mode 100644 samples/java/agents/magic_8_ball_security/client/pom.xml
create mode 100644 samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/KeycloakOAuth2CredentialService.java
create mode 100644 samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/TestClient.java
create mode 100644 samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/TestClientRunner.java
create mode 100644 samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/package-info.java
create mode 100644 samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/util/CachedToken.java
create mode 100644 samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/util/EventHandlerUtil.java
create mode 100644 samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/util/KeycloakUtil.java
create mode 100644 samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/util/package-info.java
create mode 100644 samples/java/agents/magic_8_ball_security/client/src/main/resources/keycloak.json
create mode 100644 samples/java/agents/magic_8_ball_security/pom.xml
create mode 100644 samples/java/agents/magic_8_ball_security/server/.env.example
create mode 100644 samples/java/agents/magic_8_ball_security/server/pom.xml
create mode 100644 samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/Magic8BallAgent.java
create mode 100644 samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/Magic8BallAgentCardProducer.java
create mode 100644 samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/Magic8BallAgentExecutorProducer.java
create mode 100644 samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/Magic8BallTools.java
create mode 100644 samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/package-info.java
create mode 100644 samples/java/agents/magic_8_ball_security/server/src/main/resources/application.properties
create mode 100644 samples/java/agents/pom.xml
diff --git a/samples/java/agents/README.md b/samples/java/agents/README.md
index 2a3d80aa..d405553a 100644
--- a/samples/java/agents/README.md
+++ b/samples/java/agents/README.md
@@ -19,6 +19,9 @@ Each agent can be run as its own A2A server with the instructions in its README.
Sample agent that can roll dice of different sizes and check if numbers are prime. This agent demonstrates
multi-transport capabilities.
+* [**Magic 8 Ball Agent (Security)**](magic_8_ball_security/README.md)
+ Sample agent that can respond to yes/no questions by consulting a Magic 8 Ball. This sample demonstrates how to secure an A2A server with Keycloak using bearer token authentication and it shows how to configure an A2A client to specify the token when sending requests.
+
## Disclaimer
Important: The sample code provided is for demonstration purposes and illustrates the
diff --git a/samples/java/agents/content_editor/pom.xml b/samples/java/agents/content_editor/pom.xml
index abd1e323..afe616fa 100644
--- a/samples/java/agents/content_editor/pom.xml
+++ b/samples/java/agents/content_editor/pom.xml
@@ -4,31 +4,13 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
- com.samples.a2a
- content-editor
- 0.1.0
-
-
- 17
- 17
- UTF-8
- 0.3.0.Beta1
- 4.1.0
- 3.22.3
- 1.0.0
-
+
+ com.samples.a2a
+ agents-parent
+ 0.1.0
+
-
-
-
- io.quarkus
- quarkus-bom
- ${quarkus.platform.version}
- pom
- import
-
-
-
+ content-editor
@@ -57,25 +39,10 @@
org.apache.maven.plugins
maven-compiler-plugin
- 3.13.0
-
- 17
-
io.quarkus
quarkus-maven-plugin
- ${quarkus.platform.version}
- true
-
-
-
- build
- generate-code
- generate-code-tests
-
-
-
diff --git a/samples/java/agents/content_writer/pom.xml b/samples/java/agents/content_writer/pom.xml
index 548523e6..2a371e69 100644
--- a/samples/java/agents/content_writer/pom.xml
+++ b/samples/java/agents/content_writer/pom.xml
@@ -4,31 +4,13 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
- com.samples.a2a
- content-writer
- 0.1.0
-
-
- 17
- 17
- UTF-8
- 0.3.0.Beta1
- 4.1.0
- 3.22.3
- 1.0.0
-
+
+ com.samples.a2a
+ agents-parent
+ 0.1.0
+
-
-
-
- io.quarkus
- quarkus-bom
- ${quarkus.platform.version}
- pom
- import
-
-
-
+ content-writer
@@ -57,25 +39,10 @@
org.apache.maven.plugins
maven-compiler-plugin
- 3.13.0
-
- 17
-
io.quarkus
quarkus-maven-plugin
- ${quarkus.platform.version}
- true
-
-
-
- build
- generate-code
- generate-code-tests
-
-
-
diff --git a/samples/java/agents/dice_agent_multi_transport/client/src/main/java/com/samples/a2a/client/TestClientRunner.java b/samples/java/agents/dice_agent_multi_transport/client/src/main/java/com/samples/a2a/client/TestClientRunner.java
index bb5a1d77..44f7f6e7 100644
--- a/samples/java/agents/dice_agent_multi_transport/client/src/main/java/com/samples/a2a/client/TestClientRunner.java
+++ b/samples/java/agents/dice_agent_multi_transport/client/src/main/java/com/samples/a2a/client/TestClientRunner.java
@@ -1,7 +1,7 @@
///usr/bin/env jbang "$0" "$@" ; exit $?
-//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.0.Beta1
-//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.0.Beta1
-//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-grpc:0.3.0.Beta1
+//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.0.Beta2
+//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.0.Beta2
+//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-grpc:0.3.0.Beta2
//DEPS com.fasterxml.jackson.core:jackson-databind:2.15.2
//DEPS io.grpc:grpc-netty-shaded:1.69.1
//SOURCES TestClient.java
diff --git a/samples/java/agents/dice_agent_multi_transport/pom.xml b/samples/java/agents/dice_agent_multi_transport/pom.xml
index a9354c06..a2cc4a33 100644
--- a/samples/java/agents/dice_agent_multi_transport/pom.xml
+++ b/samples/java/agents/dice_agent_multi_transport/pom.xml
@@ -4,77 +4,17 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
- com.samples.a2a
+
+ com.samples.a2a
+ agents-parent
+ 0.1.0
+
+
dice-agent-multi-transport
- 0.1.0
pom
server
client
-
-
- 17
- 17
- UTF-8
- 4.31.1
- 0.3.0.Beta1
- 4.1.0
- 3.26.1
- 1.0.0
-
-
-
-
-
- io.quarkus
- quarkus-bom
- ${quarkus.platform.version}
- pom
- import
-
-
-
- com.google.protobuf
- protobuf-java
- ${protobuf.version}
-
-
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.13.0
-
- 17
-
-
-
- io.quarkus
- quarkus-maven-plugin
- ${quarkus.platform.version}
- true
-
-
-
- build
- generate-code
- generate-code-tests
-
-
-
-
-
- org.codehaus.mojo
- exec-maven-plugin
- 3.1.0
-
-
-
-
diff --git a/samples/java/agents/magic_8_ball_security/README.md b/samples/java/agents/magic_8_ball_security/README.md
new file mode 100644
index 00000000..fe42084f
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/README.md
@@ -0,0 +1,120 @@
+# Magic 8-Ball Security Agent
+
+This sample agent responds to yes/no questions by consulting a Magic 8-Ball.
+
+This sample demonstrates how to secure an A2A server with Keycloak using bearer token authentication and it shows how to configure an A2A client to specify the token when
+sending requests. The agent is written using Quarkus LangChain4j and makes use of the
+[A2A Java](https://github.com/a2aproject/a2a-java) SDK.
+
+## Prerequisites
+
+- Java 17 or higher
+- Access to an LLM and API Key
+- A working container runtime (Docker or [Podman](https://quarkus.io/guides/podman))
+
+>**NOTE**: We'll be making use of Quarkus Dev Services in this sample to automatically create and configure a Keycloak instance that we'll use as our OAuth2 provider. For more details on using Podman with Quarkus, see this [guide](https://quarkus.io/guides/podman).
+
+## Running the Sample
+
+This sample consists of an A2A server agent, which is in the `server` directory, and an A2A client,
+which is in the `client` directory.
+
+### Running the A2A Server Agent
+
+1. Navigate to the `magic-8-ball-security` sample directory:
+
+ ```bash
+ cd samples/java/agents/magic-8-ball-security/server
+ ```
+
+2. Set your Google AI Studio API Key as an environment variable:
+
+ ```bash
+ export QUARKUS_LANGCHAIN4J_AI_GEMINI_API_KEY=your_api_key_here
+ ```
+
+ Alternatively, you can create a `.env` file in the `magic-8-ball-security/server` directory:
+
+ ```bash
+ QUARKUS_LANGCHAIN4J_AI_GEMINI_API_KEY=your_api_key_here
+ ```
+
+3. Start the A2A server agent
+
+ **NOTE:**
+ By default, the agent will start on port 11000. To override this, add the `-Dquarkus.http.port=YOUR_PORT`
+ option at the end of the command below.
+
+ ```bash
+ mvn quarkus:dev
+ ```
+
+### Running the A2A Java Client
+
+The Java `TestClient` communicates with the Magic 8-Ball Agent using the A2A Java SDK.
+
+The client supports specifying which transport protocol to use ("jsonrpc", "rest", or "grpc"). By default, it uses JSON-RPC.
+
+1. Make sure you have [JBang installed](https://www.jbang.dev/documentation/jbang/latest/installation.html)
+
+2. Run the client using the JBang script:
+ ```bash
+ cd samples/java/agents/magic-8-ball-security/client/src/main/java/com/samples/a2a/client
+ jbang TestClientRunner.java
+ ```
+
+ Or specify a custom server URL:
+ ```bash
+ jbang TestClientRunner.java --server-url http://localhost:11000
+ ```
+
+ Or specify a custom message:
+ ```bash
+ jbang TestClientRunner.java --message "Should I refactor this code?"
+ ```
+
+ Or specify a specific transport (jsonrpc, grpc, or rest):
+ ```bash
+ jbang TestClientRunner.java --transport grpc
+ ```
+
+ Or combine multiple options:
+ ```bash
+ jbang TestClientRunner.java --server-url http://localhost:11000 --message "Will my tests pass?" --transport rest
+ ```
+
+## Expected Client Output
+
+The Java A2A client will:
+1. Connect to the Magic 8-Ball agent
+2. Fetch the agent card
+3. Use the specified transport (JSON-RPC by default, or as specified via --transport option)
+4. Send the message "Should I deploy this code on Friday?" (or your custom message)
+5. Display the Magic 8-Ball's mystical response from the agent
+
+## Keycloak OAuth2 Authentication
+
+This sample includes a `KeycloakOAuth2CredentialService` that implements the `CredentialService` interface from the A2A Java SDK to retrieve tokens from Keycloak
+using Keycloak `AuthzClient`.
+
+## Multi-Transport Support
+
+This sample demonstrates multi-transport capabilities by supporting the JSON-RPC, HTTP+JSON/REST, and gRPC transports. The A2A server agent is configured to use a unified port for all three transports.
+
+## Disclaimer
+Important: The sample code provided is for demonstration purposes and illustrates the
+mechanics of the Agent-to-Agent (A2A) protocol. When building production applications,
+it is critical to treat any agent operating outside of your direct control as a
+potentially untrusted entity.
+
+All data received from an external agent—including but not limited to its AgentCard,
+messages, artifacts, and task statuses—should be handled as untrusted input. For
+example, a malicious agent could provide an AgentCard containing crafted data in its
+fields (e.g., description, name, skills.description). If this data is used without
+sanitization to construct prompts for a Large Language Model (LLM), it could expose
+your application to prompt injection attacks. Failure to properly validate and
+sanitize this data before use can introduce security vulnerabilities into your
+application.
+
+Developers are responsible for implementing appropriate security measures, such as
+input validation and secure handling of credentials to protect their systems and users.
diff --git a/samples/java/agents/magic_8_ball_security/client/pom.xml b/samples/java/agents/magic_8_ball_security/client/pom.xml
new file mode 100644
index 00000000..0aac5388
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/client/pom.xml
@@ -0,0 +1,74 @@
+
+
+ 4.0.0
+
+
+ com.samples.a2a
+ magic-8-ball-security
+ 0.1.0
+
+
+ magic-8-ball-security-client
+ Magic 8-Ball Security Agent Client
+ A2A Magic 8-Ball Security Agent Test Client
+
+
+
+ io.github.a2asdk
+ a2a-java-sdk-client
+ ${io.a2a.sdk.version}
+
+
+ io.github.a2asdk
+ a2a-java-sdk-client-transport-rest
+ ${io.a2a.sdk.version}
+
+
+ io.github.a2asdk
+ a2a-java-sdk-client-transport-grpc
+ ${io.a2a.sdk.version}
+
+
+ io.grpc
+ grpc-netty-shaded
+ runtime
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ com.google.auth
+ google-auth-library-oauth2-http
+ 1.19.0
+
+
+ com.google.http-client
+ google-http-client-jackson2
+ 1.43.3
+
+
+ org.keycloak
+ keycloak-authz-client
+ 25.0.1
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+
+ com.samples.a2a.TestClient
+
+
+
+
+
diff --git a/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/KeycloakOAuth2CredentialService.java b/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/KeycloakOAuth2CredentialService.java
new file mode 100644
index 00000000..e553beef
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/KeycloakOAuth2CredentialService.java
@@ -0,0 +1,55 @@
+package com.samples.a2a.client;
+
+import com.samples.a2a.client.util.CachedToken;
+import com.samples.a2a.client.util.KeycloakUtil;
+import io.a2a.client.transport.spi.interceptors.ClientCallContext;
+import io.a2a.client.transport.spi.interceptors.auth.CredentialService;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import org.keycloak.authorization.client.AuthzClient;
+
+/**
+ * A CredentialService implementation that provides OAuth2 access tokens
+ * using Keycloak. This service is used by the A2A client transport
+ * authentication interceptors.
+ */
+public final class KeycloakOAuth2CredentialService implements CredentialService {
+
+ /** OAuth2 scheme name. */
+ private static final String OAUTH2_SCHEME_NAME = "oauth2";
+
+ /** Token cache. */
+ private final ConcurrentMap tokenCache
+ = new ConcurrentHashMap<>();
+
+ /** Keycloak authz client. */
+ private final AuthzClient authzClient;
+
+ /**
+ * Creates a new KeycloakOAuth2CredentialService using the
+ * default keycloak.json file.
+ *
+ * @throws IllegalArgumentException if keycloak.json cannot be found/loaded
+ */
+ public KeycloakOAuth2CredentialService() {
+ this.authzClient = KeycloakUtil.createAuthzClient();
+ }
+
+ @Override
+ public String getCredential(final String securitySchemeName,
+ final ClientCallContext clientCallContext) {
+ if (!OAUTH2_SCHEME_NAME.equals(securitySchemeName)) {
+ throw new IllegalArgumentException("Unsupported security scheme: "
+ + securitySchemeName);
+ }
+
+ try {
+ return KeycloakUtil.getAccessToken(securitySchemeName,
+ tokenCache, authzClient);
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Failed to obtain OAuth2 access token for scheme: "
+ + securitySchemeName, e);
+ }
+ }
+}
diff --git a/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/TestClient.java b/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/TestClient.java
new file mode 100644
index 00000000..429084e5
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/TestClient.java
@@ -0,0 +1,119 @@
+package com.samples.a2a.client;
+
+import com.samples.a2a.client.util.EventHandlerUtil;
+import io.a2a.client.Client;
+import io.a2a.client.ClientEvent;
+import io.a2a.client.config.ClientConfig;
+import io.a2a.client.transport.grpc.GrpcTransport;
+import io.a2a.client.transport.grpc.GrpcTransportConfigBuilder;
+import io.a2a.client.transport.jsonrpc.JSONRPCTransport;
+import io.a2a.client.transport.jsonrpc.JSONRPCTransportConfigBuilder;
+import io.a2a.client.transport.rest.RestTransport;
+import io.a2a.client.transport.rest.RestTransportConfigBuilder;
+import io.a2a.client.transport.spi.interceptors.auth.AuthInterceptor;
+import io.a2a.client.transport.spi.interceptors.auth.CredentialService;
+import io.a2a.spec.AgentCard;
+import io.grpc.Channel;
+import io.grpc.ManagedChannelBuilder;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Test client utility for creating A2A clients with HTTP-based transports
+ * and OAuth2 authentication.
+ *
+ * This class encapsulates the complexity of setting up A2A clients with
+ * multiple transport options (gRPC, REST, JSON-RPC) and Keycloak OAuth2
+ * authentication, providing simple methods to create configured clients
+ * for testing and development.
+ */
+public final class TestClient {
+
+ private TestClient() {
+ }
+
+ /**
+ * Creates an A2A client with the specified transport and
+ * OAuth2 authentication.
+ *
+ * @param agentCard the agent card to connect to
+ * @param messageResponse CompletableFuture for handling responses
+ * @param transport the transport type to use ("grpc", "rest", or "jsonrpc")
+ * @return configured A2A client
+ */
+ public static Client createClient(
+ final AgentCard agentCard,
+ final CompletableFuture messageResponse,
+ final String transport) {
+
+ // Create consumers for handling client events
+ List> consumers =
+ EventHandlerUtil.createEventConsumers(messageResponse);
+
+ // Create error handler for streaming errors
+ Consumer streamingErrorHandler =
+ EventHandlerUtil.createStreamingErrorHandler(messageResponse);
+
+ // Create credential service for OAuth2 authentication
+ CredentialService credentialService
+ = new KeycloakOAuth2CredentialService();
+
+ // Create shared auth interceptor for all transports
+ AuthInterceptor authInterceptor = new AuthInterceptor(credentialService);
+
+ // Create channel factory for gRPC transport
+ Function channelFactory =
+ agentUrl -> {
+ return ManagedChannelBuilder
+ .forTarget(agentUrl)
+ .usePlaintext()
+ .build();
+ };
+
+ // Create the A2A client with the specified transport
+ try {
+ var builder =
+ Client.builder(agentCard)
+ .addConsumers(consumers)
+ .streamingErrorHandler(streamingErrorHandler);
+
+ // Configure only the specified transport
+ switch (transport.toLowerCase()) {
+ case "grpc":
+ builder.withTransport(
+ GrpcTransport.class,
+ new GrpcTransportConfigBuilder()
+ .channelFactory(channelFactory)
+ .addInterceptor(authInterceptor) // auth config
+ .build());
+ break;
+ case "rest":
+ builder.withTransport(
+ RestTransport.class,
+ new RestTransportConfigBuilder()
+ .addInterceptor(authInterceptor) // auth config
+ .build());
+ break;
+ case "jsonrpc":
+ builder.withTransport(
+ JSONRPCTransport.class,
+ new JSONRPCTransportConfigBuilder()
+ .addInterceptor(authInterceptor) // auth config
+ .build());
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unsupported transport type: "
+ + transport
+ + ". Supported types are: grpc, rest, jsonrpc");
+ }
+
+ return builder.clientConfig(new ClientConfig.Builder().build()).build();
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create A2A client", e);
+ }
+ }
+}
diff --git a/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/TestClientRunner.java b/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/TestClientRunner.java
new file mode 100644
index 00000000..096d6655
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/TestClientRunner.java
@@ -0,0 +1,231 @@
+/// usr/bin/env jbang "$0" "$@" ; exit $?
+//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.0.Beta2
+//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.0.Beta2
+//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-grpc:0.3.0.Beta2
+//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-rest:0.3.0.Beta2
+//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-spi:0.3.0.Beta2
+//DEPS com.fasterxml.jackson.core:jackson-databind:2.15.2
+//DEPS io.grpc:grpc-netty-shaded:1.69.1
+//DEPS org.keycloak:keycloak-authz-client:25.0.1
+//SOURCES TestClient.java
+//SOURCES util/KeycloakUtil.java
+//SOURCES util/EventHandlerUtil.java
+//SOURCES util/CachedToken.java
+//SOURCES KeycloakOAuth2CredentialService.java
+//FILES ../../../../../resources/keycloak.json
+
+package com.samples.a2a.client;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.a2a.A2A;
+import io.a2a.client.Client;
+import io.a2a.client.http.A2ACardResolver;
+import io.a2a.spec.A2AClientException;
+import io.a2a.spec.AgentCard;
+import io.a2a.spec.Message;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * JBang script to run the A2A HTTP TestClient example with OAuth2
+ * authentication. This script automatically handles the dependencies
+ * and runs the client with a specified transport.
+ *
+ * This is a self-contained script that demonstrates how to:
+ *
+ *
+ * - Connect to an A2A agent using a specific transport
+ * (gRPC, REST, or JSON-RPC) with OAuth2 authentication
+ *
- Send messages and receive responses
+ *
- Handle agent interactions
+ *
+ *
+ * Prerequisites:
+ *
+ *
+ * - JBang installed
+ * (see https://www.jbang.dev/documentation/guide/latest/installation.html)
+ *
- A running Magic 8 Ball A2A server agent that supports the specified
+ * transport with OAuth2 authentication
+ *
- A valid keycloak.json configuration file in the classpath
+ *
- A running Keycloak server with properly configured client
+ *
+ *
+ * Usage:
+ *
+ *
{@code
+ * $ jbang TestClientRunner.java
+ * }
+ *
+ * Or with custom parameters:
+ *
+ *
{@code
+ * $ jbang TestClientRunner.java --server-url http://localhost:11001
+ * $ jbang TestClientRunner.java --message "Should I refactor this code?"
+ * $ jbang TestClientRunner.java --transport grpc
+ * $ jbang TestClientRunner.java --server-url http://localhost:11001
+ * --message "Will my tests pass?" --transport rest
+ * }
+ *
+ * The script will:
+ *
+ *
+ * - Create the specified transport config with auth config
+ *
- Communicate with the Magic 8 Ball A2A server agent
+ *
- Automatically include OAuth2 Bearer tokens in all requests
+ *
- Handle A2A protocol interactions and display responses
+ *
+ *
+ * The heavy lifting for client setup is handled by {@link TestClient}.
+ */
+public final class TestClientRunner {
+
+ /** The default server URL to use. */
+ private static final String DEFAULT_SERVER_URL = "http://localhost:11000";
+
+ /** The default message text to send. */
+ private static final String MESSAGE_TEXT
+ = "Should I deploy this code on Friday?";
+
+ /** The default transport to use. */
+ private static final String DEFAULT_TRANSPORT = "jsonrpc";
+
+ /** Object mapper to use. */
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ private TestClientRunner() {
+ // Utility class, prevent instantiation
+ }
+
+ /** Prints usage information and exits. */
+ private static void printUsageAndExit() {
+ System.out.println("Usage: jbang TestClientRunner.java [OPTIONS]");
+ System.out.println();
+ System.out.println("Options:");
+ System.out.println(
+ " --server-url URL The URL of the A2A server agent (default: "
+ + DEFAULT_SERVER_URL
+ + ")");
+ System.out.println(
+ " --message TEXT The message to send to the agent "
+ + "(default: \""
+ + MESSAGE_TEXT
+ + "\")");
+ System.out.println(
+ " --transport TYPE "
+ + "The transport type to use: jsonrpc, grpc, or rest "
+ + "(default: "
+ + DEFAULT_TRANSPORT
+ + ")");
+ System.out.println(" --help, -h Show this help message and exit");
+ System.out.println();
+ System.out.println("Examples:");
+ System.out.println(" jbang TestClientRunner.java "
+ + "--server-url http://localhost:11001");
+ System.out.println(" jbang TestClientRunner.java "
+ + "--message \"Should I refactor this code?\"");
+ System.out.println(" jbang TestClientRunner.java --transport grpc");
+ System.out.println(
+ " jbang TestClientRunner.java --server-url http://localhost:11001 "
+ + "--message \"Will my tests pass?\" --transport rest");
+ System.exit(0);
+ }
+
+ /**
+ * Client entry point.
+ *
+ * @param args can optionally contain the --server-url,
+ * --message, and --transport to use
+ */
+ public static void main(final String[] args) {
+ System.out.println("=== A2A Client with OAuth2 Authentication Example ===");
+
+ String serverUrl = DEFAULT_SERVER_URL;
+ String messageText = MESSAGE_TEXT;
+ String transport = DEFAULT_TRANSPORT;
+
+ // Parse command line arguments
+ for (int i = 0; i < args.length; i++) {
+ switch (args[i]) {
+ case "--server-url":
+ if (i + 1 < args.length) {
+ serverUrl = args[i + 1];
+ i++;
+ } else {
+ System.err.println("Error: --server-url requires a value");
+ printUsageAndExit();
+ }
+ break;
+ case "--message":
+ if (i + 1 < args.length) {
+ messageText = args[i + 1];
+ i++;
+ } else {
+ System.err.println("Error: --message requires a value");
+ printUsageAndExit();
+ }
+ break;
+ case "--transport":
+ if (i + 1 < args.length) {
+ transport = args[i + 1];
+ i++;
+ } else {
+ System.err.println("Error: --transport requires a value");
+ printUsageAndExit();
+ }
+ break;
+ case "--help":
+ case "-h":
+ printUsageAndExit();
+ break;
+ default:
+ System.err.println("Error: Unknown argument: " + args[i]);
+ printUsageAndExit();
+ }
+ }
+
+ try {
+ System.out.println("Connecting to agent at: " + serverUrl);
+ System.out.println("Using transport: " + transport);
+
+ // Fetch the public agent card
+ AgentCard publicAgentCard = new A2ACardResolver(serverUrl).getAgentCard();
+ System.out.println("Successfully fetched public agent card:");
+ System.out.println(OBJECT_MAPPER.writeValueAsString(publicAgentCard));
+ System.out.println("Using public agent card for client initialization.");
+
+ // Create a CompletableFuture to handle async response
+ final CompletableFuture messageResponse
+ = new CompletableFuture<>();
+
+ // Create the A2A client with the specified transport using TestClient
+ Client client = TestClient.createClient(publicAgentCard,
+ messageResponse, transport);
+
+ // Create and send the message
+ Message message = A2A.toUserMessage(messageText);
+
+ System.out.println("Sending message: " + messageText);
+ System.out.println("Using " + transport
+ + " transport with OAuth2 Bearer token");
+ try {
+ client.sendMessage(message);
+ } catch (A2AClientException e) {
+ messageResponse.completeExceptionally(e);
+ }
+ System.out.println("Message sent successfully. Waiting for response...");
+
+ try {
+ // Wait for response
+ String responseText = messageResponse.get();
+ System.out.println("Final response: " + responseText);
+ } catch (Exception e) {
+ System.err.println("Failed to get response: " + e.getMessage());
+ e.printStackTrace();
+ }
+
+ } catch (Exception e) {
+ System.err.println("An error occurred: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/package-info.java b/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/package-info.java
new file mode 100644
index 00000000..183f90b6
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/package-info.java
@@ -0,0 +1,2 @@
+/** A2A Client examples and utilities with OAuth2 authentication support. */
+package com.samples.a2a.client;
diff --git a/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/util/CachedToken.java b/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/util/CachedToken.java
new file mode 100644
index 00000000..7ee6d1bb
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/util/CachedToken.java
@@ -0,0 +1,91 @@
+package com.samples.a2a.client.util;
+
+/**
+ * Represents a cached token with expiration information.
+ *
+ * This utility class is used to cache OAuth2 access tokens and
+ * provides expiration checking to avoid using expired tokens.
+ */
+public final class CachedToken {
+ /** Expiration buffer. */
+ private static final long EXPIRATION_BUFFER_MS = 5 * 60 * 1000; // 5 minutes
+
+ /** Converstion to milliseconds. */
+ private static final long SECONDS_TO_MS = 1000;
+
+ /** Cached token. */
+ private final String token;
+
+ /** Expiration time. */
+ private final long expirationTime;
+
+ /**
+ * Creates a new CachedToken with the specified token and expiration time.
+ *
+ * @param token the access token string
+ * @param expirationTime the expiration time in milliseconds since epoch
+ */
+ public CachedToken(final String token, final long expirationTime) {
+ this.token = token;
+ this.expirationTime = expirationTime;
+ }
+
+ /**
+ * Gets the cached token.
+ *
+ * @return the access token string
+ */
+ public String getToken() {
+ return token;
+ }
+
+ /**
+ * Gets the expiration time.
+ *
+ * @return the expiration time in milliseconds since epoch
+ */
+ public long getExpirationTime() {
+ return expirationTime;
+ }
+
+ /**
+ * Checks if the token is expired or will expire soon.
+ *
+ *
Returns true if the token will expire within 5 minutes to provide
+ * a buffer for token refresh.
+ *
+ * @return true if the token is expired or will expire soon
+ */
+ public boolean isExpired() {
+ // Consider token expired if it expires within 5 minutes (300,000 ms)
+ return System.currentTimeMillis()
+ >= (expirationTime - EXPIRATION_BUFFER_MS);
+ }
+
+ /**
+ * Creates a CachedToken from an access token response
+ * with expires_in seconds.
+ *
+ * @param token the access token string
+ * @param expiresInSeconds the number of seconds until expiration
+ * @return a new CachedToken instance
+ */
+ public static CachedToken fromExpiresIn(final String token,
+ final long expiresInSeconds) {
+ long expirationTime = System.currentTimeMillis()
+ + (expiresInSeconds * SECONDS_TO_MS);
+ return new CachedToken(token, expirationTime);
+ }
+
+ @Override
+ public String toString() {
+ return "CachedToken{"
+ + "token=***"
+ + // Don't log the actual token for security
+ ", expirationTime="
+ + expirationTime
+ + ", expired="
+ + isExpired()
+ + '}';
+ }
+}
diff --git a/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/util/EventHandlerUtil.java b/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/util/EventHandlerUtil.java
new file mode 100644
index 00000000..1e434c39
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/util/EventHandlerUtil.java
@@ -0,0 +1,118 @@
+package com.samples.a2a.client.util;
+
+import io.a2a.client.ClientEvent;
+import io.a2a.client.MessageEvent;
+import io.a2a.client.TaskEvent;
+import io.a2a.client.TaskUpdateEvent;
+import io.a2a.spec.AgentCard;
+import io.a2a.spec.Artifact;
+import io.a2a.spec.Message;
+import io.a2a.spec.Part;
+import io.a2a.spec.TaskArtifactUpdateEvent;
+import io.a2a.spec.TaskStatusUpdateEvent;
+import io.a2a.spec.TextPart;
+import io.a2a.spec.UpdateEvent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+/** Utility class for handling A2A client events and responses. */
+public final class EventHandlerUtil {
+
+ private EventHandlerUtil() {
+ }
+
+ /**
+ * Creates event consumers for handling A2A client events.
+ *
+ * @param messageResponse CompletableFuture to complete
+ * @return list of event consumers
+ */
+ public static List> createEventConsumers(
+ final CompletableFuture messageResponse) {
+ List> consumers = new ArrayList<>();
+ consumers.add(
+ (event, agentCard) -> {
+ if (event instanceof MessageEvent messageEvent) {
+ Message responseMessage = messageEvent.getMessage();
+ String text = extractTextFromParts(responseMessage.getParts());
+ System.out.println("Received message: " + text);
+ messageResponse.complete(text);
+ } else if (event instanceof TaskUpdateEvent taskUpdateEvent) {
+ UpdateEvent updateEvent = taskUpdateEvent.getUpdateEvent();
+ if (updateEvent
+ instanceof TaskStatusUpdateEvent taskStatusUpdateEvent) {
+ System.out.println(
+ "Received status-update: "
+ + taskStatusUpdateEvent.getStatus().state().asString());
+ if (taskStatusUpdateEvent.isFinal()) {
+ String text = extractTextFromArtifacts(
+ taskUpdateEvent.getTask().getArtifacts());
+ messageResponse.complete(text);
+ }
+ } else if (updateEvent
+ instanceof
+ TaskArtifactUpdateEvent taskArtifactUpdateEvent) {
+ List> parts = taskArtifactUpdateEvent
+ .getArtifact()
+ .parts();
+ String text = extractTextFromParts(parts);
+ System.out.println("Received artifact-update: " + text);
+ }
+ } else if (event instanceof TaskEvent taskEvent) {
+ System.out.println("Received task event: "
+ + taskEvent.getTask().getId());
+ if (taskEvent.getTask().getStatus().state().isFinal()) {
+ String text = extractTextFromArtifacts(
+ taskEvent.getTask().getArtifacts());
+ messageResponse.complete(text);
+ }
+ }
+ });
+ return consumers;
+ }
+
+ private static String extractTextFromArtifacts(
+ final List artifacts) {
+ StringBuilder textBuilder = new StringBuilder();
+ for (Artifact artifact : artifacts) {
+ textBuilder.append(extractTextFromParts(artifact.parts()));
+ }
+ return textBuilder.toString();
+ }
+
+ /**
+ * Creates a streaming error handler for A2A client.
+ *
+ * @param messageResponse CompletableFuture to complete exceptionally on error
+ * @return error handler
+ */
+ public static Consumer createStreamingErrorHandler(
+ final CompletableFuture messageResponse) {
+ return (error) -> {
+ System.out.println("Streaming error occurred: " + error.getMessage());
+ error.printStackTrace();
+ messageResponse.completeExceptionally(error);
+ };
+ }
+
+ /**
+ * Extracts text content from a list of parts.
+ *
+ * @param parts the parts to extract text from
+ * @return concatenated text content
+ */
+ public static String extractTextFromParts(final List> parts) {
+ final StringBuilder textBuilder = new StringBuilder();
+ if (parts != null) {
+ for (final Part> part : parts) {
+ if (part instanceof TextPart textPart) {
+ textBuilder.append(textPart.getText());
+ }
+ }
+ }
+ return textBuilder.toString();
+ }
+}
diff --git a/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/util/KeycloakUtil.java b/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/util/KeycloakUtil.java
new file mode 100644
index 00000000..c6c4cb44
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/util/KeycloakUtil.java
@@ -0,0 +1,95 @@
+package com.samples.a2a.client.util;
+
+import java.io.InputStream;
+import java.util.concurrent.ConcurrentMap;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.representations.AccessTokenResponse;
+
+/** Utility class for common Keycloak operations and token caching. */
+public final class KeycloakUtil {
+
+ private KeycloakUtil() {
+ // Utility class, prevent instantiation
+ }
+
+ /**
+ * Creates a Keycloak AuthzClient from the default keycloak.json
+ * configuration file.
+ *
+ * @return a configured AuthzClient
+ * @throws IllegalArgumentException if keycloak.json cannot be found/loaded
+ */
+ public static AuthzClient createAuthzClient() {
+ return createAuthzClient("keycloak.json");
+ }
+
+ private static AuthzClient createAuthzClient(final String configFileName) {
+ try {
+ InputStream configStream = null;
+
+ // First try to load from current directory (for JBang)
+ try {
+ java.io.File configFile = new java.io.File(configFileName);
+ if (configFile.exists()) {
+ configStream = new java.io.FileInputStream(configFile);
+ }
+ } catch (Exception ignored) {
+ // Fall back to classpath
+ }
+
+ // If not found in current directory, try classpath
+ if (configStream == null) {
+ configStream = KeycloakUtil.class
+ .getClassLoader()
+ .getResourceAsStream(configFileName);
+ }
+
+ if (configStream == null) {
+ throw new IllegalArgumentException("Config file not found: "
+ + configFileName);
+ }
+
+ return AuthzClient.create(configStream);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Failed to load Keycloak configuration from " + configFileName, e);
+ }
+ }
+
+ /**
+ * Gets a valid access token for the specified cache key, using the
+ * provided cache and AuthzClient. Uses caching to avoid unnecessary
+ * token requests.
+ *
+ * @param cacheKey the cache key to use for storing/retrieving the token
+ * @param tokenCache the concurrent map to use for token caching
+ * @param authzClient the Keycloak AuthzClient to use for token requests
+ * @return a valid access token
+ * @throws RuntimeException if token acquisition fails
+ */
+ public static String getAccessToken(
+ final String cacheKey,
+ final ConcurrentMap tokenCache, final AuthzClient authzClient) {
+ CachedToken cached = tokenCache.get(cacheKey);
+
+ // Check if we have a valid cached token
+ if (cached != null && !cached.isExpired()) {
+ return cached.getToken();
+ }
+
+ try {
+ // Obtain a new access token from Keycloak
+ AccessTokenResponse tokenResponse = authzClient.obtainAccessToken();
+
+ // Cache the token with expiration info
+ CachedToken newToken =
+ CachedToken.fromExpiresIn(tokenResponse.getToken(),
+ tokenResponse.getExpiresIn());
+ tokenCache.put(cacheKey, newToken);
+
+ return tokenResponse.getToken();
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to obtain token from Keycloak", e);
+ }
+ }
+}
diff --git a/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/util/package-info.java b/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/util/package-info.java
new file mode 100644
index 00000000..745afb8e
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/util/package-info.java
@@ -0,0 +1,2 @@
+/** Auth utilities. */
+package com.samples.a2a.client.util;
diff --git a/samples/java/agents/magic_8_ball_security/client/src/main/resources/keycloak.json b/samples/java/agents/magic_8_ball_security/client/src/main/resources/keycloak.json
new file mode 100644
index 00000000..8ed695b1
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/client/src/main/resources/keycloak.json
@@ -0,0 +1,9 @@
+{
+ "realm": "quarkus",
+ "auth-server-url": "http://localhost:11001/",
+ "resource": "quarkus-app",
+ "credentials": {
+ "secret": "secret"
+ },
+ "ssl-required": "external"
+}
diff --git a/samples/java/agents/magic_8_ball_security/pom.xml b/samples/java/agents/magic_8_ball_security/pom.xml
new file mode 100644
index 00000000..e23a1557
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/pom.xml
@@ -0,0 +1,20 @@
+
+
+ 4.0.0
+
+
+ com.samples.a2a
+ agents-parent
+ 0.1.0
+
+
+ magic-8-ball-security
+ pom
+
+
+ server
+ client
+
+
diff --git a/samples/java/agents/magic_8_ball_security/server/.env.example b/samples/java/agents/magic_8_ball_security/server/.env.example
new file mode 100644
index 00000000..cb2fe891
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/server/.env.example
@@ -0,0 +1 @@
+QUARKUS_LANGCHAIN4J_AI_GEMINI_API_KEY=your_api_key_here
diff --git a/samples/java/agents/magic_8_ball_security/server/pom.xml b/samples/java/agents/magic_8_ball_security/server/pom.xml
new file mode 100644
index 00000000..796b4608
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/server/pom.xml
@@ -0,0 +1,65 @@
+
+
+ 4.0.0
+
+
+ com.samples.a2a
+ magic-8-ball-security
+ 0.1.0
+
+
+ magic-8-ball-security-server
+ Magic 8-Ball Security Agent Server
+ A2A Magic 8-Ball Security Agent Server Implementation
+
+
+
+ io.github.a2asdk
+ a2a-java-sdk-reference-jsonrpc
+ ${io.a2a.sdk.version}
+
+
+ io.github.a2asdk
+ a2a-java-sdk-reference-rest
+ ${io.a2a.sdk.version}
+
+
+ io.github.a2asdk
+ a2a-java-sdk-reference-grpc
+ ${io.a2a.sdk.version}
+
+
+ jakarta.enterprise
+ jakarta.enterprise.cdi-api
+ ${jakarta.enterprise.cdi-api.version}
+
+
+ io.quarkus
+ quarkus-oidc
+
+
+ io.quarkus
+ quarkus-security
+
+
+ io.quarkiverse.langchain4j
+ quarkus-langchain4j-ai-gemini
+ ${quarkus.langchain4j.version}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ io.quarkus
+ quarkus-maven-plugin
+
+
+
+
diff --git a/samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/Magic8BallAgent.java b/samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/Magic8BallAgent.java
new file mode 100644
index 00000000..acef32c3
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/Magic8BallAgent.java
@@ -0,0 +1,41 @@
+package com.samples.a2a;
+
+import dev.langchain4j.service.MemoryId;
+import dev.langchain4j.service.SystemMessage;
+import dev.langchain4j.service.UserMessage;
+import io.quarkiverse.langchain4j.RegisterAiService;
+import jakarta.enterprise.context.ApplicationScoped;
+
+/** Magic 8 Ball fortune-telling agent. */
+@RegisterAiService(tools = Magic8BallTools.class)
+@ApplicationScoped
+public interface Magic8BallAgent {
+
+ /**
+ * Answers questions using the mystical powers of the Magic 8 Ball.
+ *
+ * @param memoryId unique identifier for this conversation
+ * @param question the users' question
+ * @return the Magic 8 Ball's response
+ */
+ @SystemMessage(
+ """
+ You shake a Magic 8 Ball to answer questions.
+ The only thing you do is shake the Magic 8 Ball to answer
+ the user's question and then discuss the response.
+ When you are asked to answer a question, you must call the
+ shakeMagic8Ball tool with the user's question.
+ You should never rely on the previous history for Magic 8 Ball
+ responses. Call the shakeMagic8Ball tool for each question.
+ You should never shake the Magic 8 Ball on your own.
+ You must always call the tool.
+ When you are asked a question, you should always make the following
+ function call:
+ 1. You should first call the shakeMagic8Ball tool to get the response.
+ Wait for the function response.
+ 2. After you get the function response, relay the response to the user.
+ You should not rely on the previous history for Magic 8 Ball responses.
+ """)
+ String answerQuestion(@MemoryId String memoryId,
+ @UserMessage String question);
+}
diff --git a/samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/Magic8BallAgentCardProducer.java b/samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/Magic8BallAgentCardProducer.java
new file mode 100644
index 00000000..5bb5decf
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/Magic8BallAgentCardProducer.java
@@ -0,0 +1,96 @@
+package com.samples.a2a;
+
+import io.a2a.server.PublicAgentCard;
+import io.a2a.spec.AgentCapabilities;
+import io.a2a.spec.AgentCard;
+import io.a2a.spec.AgentInterface;
+import io.a2a.spec.AgentSkill;
+import io.a2a.spec.ClientCredentialsOAuthFlow;
+import io.a2a.spec.OAuth2SecurityScheme;
+import io.a2a.spec.OAuthFlows;
+import io.a2a.spec.TransportProtocol;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Produces;
+import jakarta.inject.Inject;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+/** Producer for Magic 8 Ball agent card configuration. */
+@ApplicationScoped
+public final class Magic8BallAgentCardProducer {
+
+ /** The HTTP port for the agent service. */
+ @Inject
+ @ConfigProperty(name = "quarkus.http.port")
+ private int httpPort;
+
+ /** The HTTP port for Keycloak. */
+ @Inject
+ @ConfigProperty(name = "quarkus.keycloak.devservices.port")
+ private int keycloakPort;
+
+ /**
+ * Produces the agent card for the Magic 8 Ball agent.
+ *
+ * @return the configured agent card
+ */
+ @Produces
+ @PublicAgentCard
+ public AgentCard agentCard() {
+ ClientCredentialsOAuthFlow clientCredentialsOAuthFlow = new ClientCredentialsOAuthFlow(
+ null,
+ Map.of("openid", "openid", "profile", "profile"),
+ "http://localhost:" + keycloakPort + "/realms/quarkus/protocol/openid-connect/token");
+ OAuth2SecurityScheme securityScheme = new OAuth2SecurityScheme.Builder()
+ .flows(new OAuthFlows.Builder().clientCredentials(clientCredentialsOAuthFlow).build())
+ .build();
+
+ return new AgentCard.Builder()
+ .name("Magic 8 Ball Agent")
+ .description(
+ "A mystical fortune-telling agent that answers your yes/no "
+ + "questions by asking the all-knowing Magic 8 Ball oracle.")
+ .preferredTransport(TransportProtocol.JSONRPC.asString())
+ .url("http://localhost:" + httpPort)
+ .version("1.0.0")
+ .documentationUrl("http://example.com/docs")
+ .capabilities(
+ new AgentCapabilities.Builder()
+ .streaming(true)
+ .pushNotifications(false)
+ .stateTransitionHistory(false)
+ .build())
+ .defaultInputModes(List.of("text"))
+ .defaultOutputModes(List.of("text"))
+ .security(List.of(Map.of(OAuth2SecurityScheme.OAUTH2,
+ List.of("profile"))))
+ .securitySchemes(Map.of(OAuth2SecurityScheme.OAUTH2, securityScheme))
+ .skills(
+ List.of(
+ new AgentSkill.Builder()
+ .id("magic_8_ball")
+ .name("Magic 8 Ball Fortune Teller")
+ .description("Uses a Magic 8 Ball to answer"
+ + " yes/no questions")
+ .tags(List.of("fortune", "magic-8-ball", "oracle"))
+ .examples(
+ List.of(
+ "Should I deploy this code on Friday?",
+ "Will my tests pass?",
+ "Is this a good idea?"))
+ .build()))
+ .protocolVersion("0.3.0")
+ .additionalInterfaces(
+ List.of(
+ new AgentInterface(
+ TransportProtocol.JSONRPC.asString(),
+ "http://localhost:" + httpPort),
+ new AgentInterface(
+ TransportProtocol.HTTP_JSON.asString(),
+ "http://localhost:" + httpPort),
+ new AgentInterface(TransportProtocol.GRPC.asString(),
+ "localhost:" + httpPort)))
+ .build();
+ }
+}
diff --git a/samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/Magic8BallAgentExecutorProducer.java b/samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/Magic8BallAgentExecutorProducer.java
new file mode 100644
index 00000000..3711128c
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/Magic8BallAgentExecutorProducer.java
@@ -0,0 +1,118 @@
+package com.samples.a2a;
+
+import io.a2a.server.agentexecution.AgentExecutor;
+import io.a2a.server.agentexecution.RequestContext;
+import io.a2a.server.events.EventQueue;
+import io.a2a.server.tasks.TaskUpdater;
+import io.a2a.spec.JSONRPCError;
+import io.a2a.spec.Message;
+import io.a2a.spec.Part;
+import io.a2a.spec.Task;
+import io.a2a.spec.TaskNotCancelableError;
+import io.a2a.spec.TaskState;
+import io.a2a.spec.TextPart;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Produces;
+import jakarta.inject.Inject;
+import java.util.List;
+import java.util.UUID;
+
+/** Producer for Magic 8 Ball agent executor. */
+@ApplicationScoped
+public final class Magic8BallAgentExecutorProducer {
+
+ /** The Magic 8 Ball agent instance. */
+ @Inject private Magic8BallAgent magic8BallAgent;
+
+ /**
+ * Produces the agent executor for the Magic 8 Ball agent.
+ *
+ * @return the configured agent executor
+ */
+ @Produces
+ public AgentExecutor agentExecutor() {
+ return new Magic8BallAgentExecutor(magic8BallAgent);
+ }
+
+ /** Magic 8 Ball agent executor implementation. */
+ private static class Magic8BallAgentExecutor implements AgentExecutor {
+
+ /** The Magic 8 Ball agent instance. */
+ private final Magic8BallAgent agent;
+
+ /**
+ * Constructor for Magic8BallAgentExecutor.
+ *
+ * @param magic8BallAgentInstance the Magic 8 Ball agent instance
+ */
+ Magic8BallAgentExecutor(final Magic8BallAgent magic8BallAgentInstance) {
+ this.agent = magic8BallAgentInstance;
+ }
+
+ @Override
+ public void execute(final RequestContext context,
+ final EventQueue eventQueue)
+ throws JSONRPCError {
+ final TaskUpdater updater = new TaskUpdater(context, eventQueue);
+
+ // mark the task as submitted and start working on it
+ if (context.getTask() == null) {
+ updater.submit();
+ }
+ updater.startWork();
+
+ // extract the text from the message
+ final String question = extractTextFromMessage(context.getMessage());
+
+ // Generate a unique memory ID for this request for fresh chat memory
+ final String memoryId = UUID.randomUUID().toString();
+ System.out.println(
+ "=== EXECUTOR === Using memory ID: "
+ + memoryId + " for question: " + question);
+
+ // call the Magic 8 Ball agent with the question
+ final String response = agent.answerQuestion(memoryId, question);
+
+ // create the response part
+ final TextPart responsePart = new TextPart(response, null);
+ final List> parts = List.of(responsePart);
+
+ // add the response as an artifact and complete the task
+ updater.addArtifact(parts, null, null, null);
+ updater.complete();
+ }
+
+ private String extractTextFromMessage(final Message message) {
+ final StringBuilder textBuilder = new StringBuilder();
+ if (message.getParts() != null) {
+ for (final Part> part : message.getParts()) {
+ if (part instanceof TextPart textPart) {
+ textBuilder.append(textPart.getText());
+ }
+ }
+ }
+ return textBuilder.toString();
+ }
+
+ @Override
+ public void cancel(final RequestContext context,
+ final EventQueue eventQueue)
+ throws JSONRPCError {
+ final Task task = context.getTask();
+
+ if (task.getStatus().state() == TaskState.CANCELED) {
+ // task already cancelled
+ throw new TaskNotCancelableError();
+ }
+
+ if (task.getStatus().state() == TaskState.COMPLETED) {
+ // task already completed
+ throw new TaskNotCancelableError();
+ }
+
+ // cancel the task
+ final TaskUpdater updater = new TaskUpdater(context, eventQueue);
+ updater.cancel();
+ }
+ }
+}
diff --git a/samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/Magic8BallTools.java b/samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/Magic8BallTools.java
new file mode 100644
index 00000000..3e5be00a
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/Magic8BallTools.java
@@ -0,0 +1,59 @@
+package com.samples.a2a;
+
+import dev.langchain4j.agent.tool.Tool;
+import jakarta.enterprise.context.ApplicationScoped;
+import java.util.concurrent.ThreadLocalRandom;
+
+/** Service class that provides Magic 8 Ball fortune-telling functionality. */
+@ApplicationScoped
+public class Magic8BallTools {
+
+ /** All possible Magic 8 Ball responses. */
+ private static final String[] RESPONSES = {
+ // Positive responses (10)
+ "It is certain",
+ "It is decidedly so",
+ "Without a doubt",
+ "Yes definitely",
+ "You may rely on it",
+ "As I see it, yes",
+ "Most likely",
+ "Outlook good",
+ "Yes",
+ "Signs point to yes",
+
+ // Negative responses (5)
+ "Don't count on it",
+ "My reply is no",
+ "My sources say no",
+ "Outlook not so good",
+ "Very doubtful",
+
+ // Non-committal responses (5)
+ "Better not tell you now",
+ "Cannot predict now",
+ "Concentrate and ask again",
+ "Ask again later",
+ "Reply hazy, try again"
+ };
+
+ /**
+ * Get the response from the Magic 8 Ball.
+ *
+ * @param question the user's question
+ * @return A random Magic 8 Ball response
+ */
+ @Tool("Get the response to the user's question from the Magic 8 Ball")
+ public String shakeMagic8Ball(final String question) {
+ int index = ThreadLocalRandom.current().nextInt(RESPONSES.length);
+ String response = RESPONSES[index];
+ System.out.println(
+ "=== TOOL CALLED === Question: "
+ + question
+ + ", Index: "
+ + index
+ + ", Response: "
+ + response);
+ return response;
+ }
+}
diff --git a/samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/package-info.java b/samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/package-info.java
new file mode 100644
index 00000000..7079e6f4
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/server/src/main/java/com/samples/a2a/package-info.java
@@ -0,0 +1,2 @@
+/** Magic 8 Ball package. */
+package com.samples.a2a;
diff --git a/samples/java/agents/magic_8_ball_security/server/src/main/resources/application.properties b/samples/java/agents/magic_8_ball_security/server/src/main/resources/application.properties
new file mode 100644
index 00000000..d1ac5123
--- /dev/null
+++ b/samples/java/agents/magic_8_ball_security/server/src/main/resources/application.properties
@@ -0,0 +1,7 @@
+# Use the same port for gRPC and HTTP
+quarkus.grpc.server.use-separate-server=false
+quarkus.http.port=11000
+quarkus.langchain4j.ai.gemini.timeout=42543
+quarkus.keycloak.devservices.port=11001
+quarkus.langchain4j.ai.gemini.chat-model.model-id=gemini-2.5-flash
+
diff --git a/samples/java/agents/pom.xml b/samples/java/agents/pom.xml
new file mode 100644
index 00000000..faf5b293
--- /dev/null
+++ b/samples/java/agents/pom.xml
@@ -0,0 +1,87 @@
+
+
+ 4.0.0
+
+ com.samples.a2a
+ agents-parent
+ 0.1.0
+ pom
+
+
+ content_editor
+ content_writer
+ dice_agent_multi_transport
+ magic_8_ball_security
+ weather_mcp
+
+
+
+ 17
+ 17
+ UTF-8
+
+
+ 0.3.0.Beta2
+
+
+ 4.1.0
+ 3.26.1
+ 1.3.1
+ 4.31.1
+
+
+
+
+
+ io.quarkus
+ quarkus-bom
+ ${quarkus.platform.version}
+ pom
+ import
+
+
+
+ com.google.protobuf
+ protobuf-java
+ ${protobuf.version}
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ 17
+
+
+
+ io.quarkus
+ quarkus-maven-plugin
+ ${quarkus.platform.version}
+ true
+
+
+
+ build
+ generate-code
+ generate-code-tests
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+
+
+
+
diff --git a/samples/java/agents/weather_mcp/pom.xml b/samples/java/agents/weather_mcp/pom.xml
index 7d4664c7..44cdf755 100644
--- a/samples/java/agents/weather_mcp/pom.xml
+++ b/samples/java/agents/weather_mcp/pom.xml
@@ -4,31 +4,13 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
- com.samples.a2a
- weather
- 0.1.0
-
-
- 17
- 17
- UTF-8
- 0.3.0.Beta1
- 4.1.0
- 3.22.3
- 1.0.0
-
+
+ com.samples.a2a
+ agents-parent
+ 0.1.0
+
-
-
-
- io.quarkus
- quarkus-bom
- ${quarkus.platform.version}
- pom
- import
-
-
-
+ weather
@@ -66,25 +48,10 @@
org.apache.maven.plugins
maven-compiler-plugin
- 3.13.0
-
- 17
-
io.quarkus
quarkus-maven-plugin
- ${quarkus.platform.version}
- true
-
-
-
- build
- generate-code
- generate-code-tests
-
-
-
From 27a0a11d3c7653a6c6e66890b5d6c15ed9319de9 Mon Sep 17 00:00:00 2001
From: Farah Juma
Date: Fri, 24 Oct 2025 14:24:40 -0400
Subject: [PATCH 11/14] fix: Fix the path to the Weather Agent in the
weather_and_airbnb_planner README and update the URL for the UI (#393)
# Description
Thank you for opening a Pull Request!
Before submitting your PR, there are a few things you can do to make
sure it goes smoothly:
- [x] Follow the [`CONTRIBUTING`
Guide](https://github.com/a2aproject/a2a-samples/blob/main/CONTRIBUTING.md).
---
samples/python/hosts/weather_and_airbnb_planner/README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/samples/python/hosts/weather_and_airbnb_planner/README.md b/samples/python/hosts/weather_and_airbnb_planner/README.md
index e75f1219..f293638d 100644
--- a/samples/python/hosts/weather_and_airbnb_planner/README.md
+++ b/samples/python/hosts/weather_and_airbnb_planner/README.md
@@ -136,7 +136,7 @@ Run the airbnb agent server:
Open a new terminal and run the weather agent:
```bash
- cd samples/multi_language/python_and_java_multiagent/weather_agent
+ cd samples/java/agents/weather_mcp
mvn quarkus:dev
```
@@ -152,7 +152,7 @@ Open a new terminal and run the host agent server:
## 5. Test using the UI
-From your browser, navigate to .
+From your browser, navigate to .
Here are example questions:
From 8684c27f2070daf14326f66a2d08cd15f9b31fce Mon Sep 17 00:00:00 2001
From: Farah Juma
Date: Tue, 28 Oct 2025 11:24:12 -0400
Subject: [PATCH 12/14] Update the a2a-java samples to A2A Java SDK 0.3.0.Final
(#395)
# Description
Thank you for opening a Pull Request!
Before submitting your PR, there are a few things you can do to make
sure it goes smoothly:
- [x] Follow the [`CONTRIBUTING`
Guide](https://github.com/a2aproject/a2a-samples/blob/main/CONTRIBUTING.md).
---
.../java/com/samples/a2a/client/TestClientRunner.java | 6 +++---
.../java/com/samples/a2a/client/TestClientRunner.java | 10 +++++-----
samples/java/agents/pom.xml | 2 +-
3 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/samples/java/agents/dice_agent_multi_transport/client/src/main/java/com/samples/a2a/client/TestClientRunner.java b/samples/java/agents/dice_agent_multi_transport/client/src/main/java/com/samples/a2a/client/TestClientRunner.java
index 44f7f6e7..a945106d 100644
--- a/samples/java/agents/dice_agent_multi_transport/client/src/main/java/com/samples/a2a/client/TestClientRunner.java
+++ b/samples/java/agents/dice_agent_multi_transport/client/src/main/java/com/samples/a2a/client/TestClientRunner.java
@@ -1,7 +1,7 @@
///usr/bin/env jbang "$0" "$@" ; exit $?
-//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.0.Beta2
-//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.0.Beta2
-//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-grpc:0.3.0.Beta2
+//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.0.Final
+//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.0.Final
+//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-grpc:0.3.0.Final
//DEPS com.fasterxml.jackson.core:jackson-databind:2.15.2
//DEPS io.grpc:grpc-netty-shaded:1.69.1
//SOURCES TestClient.java
diff --git a/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/TestClientRunner.java b/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/TestClientRunner.java
index 096d6655..7ee5a208 100644
--- a/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/TestClientRunner.java
+++ b/samples/java/agents/magic_8_ball_security/client/src/main/java/com/samples/a2a/client/TestClientRunner.java
@@ -1,9 +1,9 @@
/// usr/bin/env jbang "$0" "$@" ; exit $?
-//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.0.Beta2
-//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.0.Beta2
-//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-grpc:0.3.0.Beta2
-//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-rest:0.3.0.Beta2
-//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-spi:0.3.0.Beta2
+//DEPS io.github.a2asdk:a2a-java-sdk-client:0.3.0.Final
+//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:0.3.0.Final
+//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-grpc:0.3.0.Final
+//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-rest:0.3.0.Final
+//DEPS io.github.a2asdk:a2a-java-sdk-client-transport-spi:0.3.0.Final
//DEPS com.fasterxml.jackson.core:jackson-databind:2.15.2
//DEPS io.grpc:grpc-netty-shaded:1.69.1
//DEPS org.keycloak:keycloak-authz-client:25.0.1
diff --git a/samples/java/agents/pom.xml b/samples/java/agents/pom.xml
index faf5b293..90ccf5a5 100644
--- a/samples/java/agents/pom.xml
+++ b/samples/java/agents/pom.xml
@@ -23,7 +23,7 @@
UTF-8
- 0.3.0.Beta2
+ 0.3.0.Final
4.1.0
From eb3885fd019096d1abf092d476e7f5454d252c66 Mon Sep 17 00:00:00 2001
From: Andrey Bragin
Date: Tue, 28 Oct 2025 16:24:54 +0100
Subject: [PATCH 13/14] Add Koog Framework Examples (#379)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Add Koog Framework Examples to Samples
This PR introduces comprehensive examples demonstrating A2A protocol
implementation using [Koog](https://koog.ai/), JetBrains' open-source
agentic framework for building enterprise-ready AI agents, targeting JVM
backend, Android, iOS, JS, and WasmJS.
## What's Added
Two progressive examples showing different A2A communication patterns:
### 1. Simple Joke Agent (`simplejoke`)
- Basic message-based A2A communication
- Direct request-response pattern using `sendMessage()`
- Demonstrates minimal A2A server setup with AgentCard and message
storage
### 2. Advanced Joke Agent (`advancedjoke`)
- Full task-based A2A workflow implementation
- Graph-based agent architecture using Koog's `GraphAIAgent`
- Complete task lifecycle: Submitted → Working → InputRequired →
Completed
- Interactive clarifications via InputRequired state
- Streaming task events and artifact delivery
- Structured LLM outputs with type-safe parsing
## Key Features Demonstrated
- **A2A Protocol Integration**: Both simple message-based and advanced
task-based workflows
- **Graph-based Agent Design**: Maintainable, visual agent logic using
nodes and edges
- **Koog's Prompt DSL**: Type-safe prompt building with automatic
context management
## Why Koog?
Adds diversity to A2A samples by showcasing:
- JVM/Kotlin ecosystem representation
- Type-safe agent development with compile-time guarantees
- Multi-platform deployment options beyond Node.js/Python
Each example includes detailed inline documentation and runnable Gradle
tasks for immediate testing.
---
.editorconfig | 36 ++
samples/java/koog/.gitignore | 3 +
samples/java/koog/README.md | 114 +++++
samples/java/koog/build.gradle.kts | 43 ++
samples/java/koog/gradle.properties | 7 +
samples/java/koog/gradle/libs.versions.toml | 29 ++
.../koog/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes
.../gradle/wrapper/gradle-wrapper.properties | 7 +
samples/java/koog/gradlew | 251 +++++++++++
samples/java/koog/gradlew.bat | 94 ++++
samples/java/koog/settings.gradle.kts | 15 +
.../ai/koog/example/advancedjoke/Client.kt | 160 +++++++
.../advancedjoke/JokeWriterAgentExecutor.kt | 425 ++++++++++++++++++
.../ai/koog/example/advancedjoke/Server.kt | 87 ++++
.../ai/koog/example/simplejoke/Client.kt | 94 ++++
.../ai/koog/example/simplejoke/Server.kt | 85 ++++
.../simplejoke/SimpleJokeAgentExecutor.kt | 79 ++++
.../java/koog/src/main/resources/logback.xml | 11 +
18 files changed, 1540 insertions(+)
create mode 100644 .editorconfig
create mode 100644 samples/java/koog/.gitignore
create mode 100644 samples/java/koog/README.md
create mode 100644 samples/java/koog/build.gradle.kts
create mode 100644 samples/java/koog/gradle.properties
create mode 100644 samples/java/koog/gradle/libs.versions.toml
create mode 100644 samples/java/koog/gradle/wrapper/gradle-wrapper.jar
create mode 100644 samples/java/koog/gradle/wrapper/gradle-wrapper.properties
create mode 100755 samples/java/koog/gradlew
create mode 100644 samples/java/koog/gradlew.bat
create mode 100644 samples/java/koog/settings.gradle.kts
create mode 100644 samples/java/koog/src/main/kotlin/ai/koog/example/advancedjoke/Client.kt
create mode 100644 samples/java/koog/src/main/kotlin/ai/koog/example/advancedjoke/JokeWriterAgentExecutor.kt
create mode 100644 samples/java/koog/src/main/kotlin/ai/koog/example/advancedjoke/Server.kt
create mode 100644 samples/java/koog/src/main/kotlin/ai/koog/example/simplejoke/Client.kt
create mode 100644 samples/java/koog/src/main/kotlin/ai/koog/example/simplejoke/Server.kt
create mode 100644 samples/java/koog/src/main/kotlin/ai/koog/example/simplejoke/SimpleJokeAgentExecutor.kt
create mode 100644 samples/java/koog/src/main/resources/logback.xml
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..3050c14e
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,36 @@
+root = true
+
+[*.{kt,kts}]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+max_line_length = 120
+
+ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
+
+# Disable wildcard imports entirely
+ij_kotlin_name_count_to_use_star_import = 2147483647
+ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
+ij_kotlin_packages_to_use_import_on_demand = unset
+
+ktlint_code_style = ktlint_official
+ktlint_standard_annotation = disabled
+ktlint_standard_class-naming = disabled
+ktlint_standard_class-signature = disabled
+ktlint_standard_filename = disabled
+ktlint_standard_function-expression-body = disabled
+ktlint_standard_function-signature = disabled
+ktlint_standard_if-else-bracing = enabled
+ktlint_standard_if-else-wrapping = enabled
+ktlint_standard_no-consecutive-comments = disabled
+ktlint_standard_no-single-line-block-comment = disabled
+ktlint_standard_property-naming = disabled
+ktlint_standard_trailing-comma-on-call-site = disabled
+ktlint_standard_trailing-comma-on-declaration-site = disabled
+ktlint_standard_try-catch-finally-spacing = enabled
+ktlint_standard_backing-property-naming = disabled
+
+[**/build/**/*]
+ktlint = disabled
\ No newline at end of file
diff --git a/samples/java/koog/.gitignore b/samples/java/koog/.gitignore
new file mode 100644
index 00000000..00a5ece3
--- /dev/null
+++ b/samples/java/koog/.gitignore
@@ -0,0 +1,3 @@
+/.gradle
+/build
+/.kotlin
diff --git a/samples/java/koog/README.md b/samples/java/koog/README.md
new file mode 100644
index 00000000..b3031946
--- /dev/null
+++ b/samples/java/koog/README.md
@@ -0,0 +1,114 @@
+# Agent-to-Agent (A2A) with Koog Framework Examples
+
+This project demonstrates how to build A2A-enabled agents using [Koog](https://github.com/JetBrains/koog), the official JetBrains' framework for building predictable,
+fault-tolerant, and enterprise-ready AI agents, targeting JVM backend, Android, iOS, JS, and WasmJS.
+
+## What is Koog?
+
+Koog is JetBrains' open-source agentic framework that empowers developers to build AI agents using Kotlin. It provides:
+
+- **Graph-based agent architecture**: Define agent behavior as a graph of nodes and edges with type-safe inputs and outputs,
+ making complex workflows easier to understand and maintain
+- **Multi-platform support**: Deploy agents across JVM, Android, native iOS, JS, and WasmJS using Kotlin Multiplatform
+- **Fault tolerance**: Built-in retry mechanisms and agent state persistence for reliable execution, allowing to recover
+ crashed agents even on another machine.
+- **Prompt DSL**: Clean, type-safe DSL for building LLM prompts and automatically managing conversation context
+- **Enterprise integrations**: Works seamlessly with Spring Boot, Ktor, and other JVM frameworks
+- **Advanced Observability**: Built-in integrations with enterprise observability tools like Langfuse and W&B Weave via OpenTelemetry
+- **A2A protocol support**: Built-in support for Agent-to-Agent communication via the A2A protocol
+
+Learn more at [koog.ai](https://koog.ai/)
+
+## Prerequisites
+
+- JDK 17 or higher
+- Set `GOOGLE_API_KEY` environment variable (or configure other LLM providers in the code)
+
+## Examples
+
+### Simple Joke Agent: [simplejoke](./src/main/kotlin/ai/koog/example/simplejoke)
+
+A basic example demonstrating message-based A2A communication without task workflows.
+
+**What it demonstrates:**
+- Creating an `AgentExecutor` that wraps LLM calls using Koog's prompt DSL
+- Setting up an A2A server with an `AgentCard` that describes agent capabilities
+- Managing conversation context with message storage
+- Simple request-response pattern using `sendMessage()`
+
+**Run:**
+```bash
+# Terminal 1: Start server (port 9998)
+./gradlew runExampleSimpleJokeServer
+
+# Terminal 2: Run client
+./gradlew runExampleSimpleJokeClient
+```
+
+### Advanced Joke Agent: [advancedjoke](./src/main/kotlin/ai/koog/example/advancedjoke)
+
+A sophisticated example showcasing task-based A2A workflows using Koog's graph-based agent architecture.
+
+**What it demonstrates:**
+- **Graph-based agent design**: Uses Koog's `GraphAIAgent` with nodes and edges to create a maintainable workflow
+- **Task lifecycle management**: Full A2A task states (Submitted → Working → InputRequired → Completed)
+- **Interactive clarification**: Agent can request additional information using the InputRequired state
+- **Structured LLM outputs**: Uses sealed interfaces with `nodeLLMRequestStructured` for type-safe agent decisions
+- **Artifact delivery**: Returns final results as A2A artifacts
+- **Streaming events**: Sends real-time task updates via `sendTaskEvent()`
+
+**Run:**
+```bash
+# Terminal 1: Start server (port 9999)
+./gradlew runExampleAdvancedJokeServer
+
+# Terminal 2: Run client
+./gradlew runExampleAdvancedJokeClient
+```
+
+## Key Patterns & Koog Concepts
+
+### A2A Communication Patterns
+
+**Simple Agent:** `sendMessage()` → single response
+**Advanced Agent:** `sendMessageStreaming()` → Flow of events (Task, TaskStatusUpdateEvent, TaskArtifactUpdateEvent)
+
+**Task States:** Submitted → Working → InputRequired (optional) → Completed
+
+### Koog Framework Concepts Used
+
+**AgentExecutor**: The entry point for A2A requests. Receives the request context and event processor for sending responses.
+
+**GraphAIAgent**: Koog's graph-based agent implementation. Define your agent logic as nodes (processing steps) connected by edges (transitions).
+
+**Prompt DSL**: Type-safe Kotlin DSL for building prompts:
+```kotlin
+prompt("joke-generation") {
+ system { +"You are a helpful assistant" }
+ user { +"Tell me a joke" }
+}
+```
+
+**MultiLLMPromptExecutor**: Unified interface for executing prompts across different LLM providers (OpenAI, Anthropic, Google, etc.).
+
+**nodeLLMRequestStructured**: Creates a graph node that calls the LLM and parses the response into a structured Kotlin data class using the `@LLMDescription` annotation.
+
+**A2AAgentServer plugin**: Koog plugin that integrates A2A functionality into your GraphAIAgent, providing access to message storage, task storage, and event processors.
+
+### Getting Started with Koog
+
+To build your own A2A agent with Koog:
+
+1. **Add Koog dependencies** (see [build.gradle.kts](./build.gradle.kts))
+2. **Create an AgentExecutor** to handle incoming A2A requests
+3. **Define an AgentCard** describing your agent's capabilities
+4. **Set up the A2A server** with HTTP transport
+5. **For simple agents**: Use prompt executor directly with message storage
+6. **For complex agents**: Use GraphAIAgent with the A2AAgentServer plugin
+
+See the code comments in `JokeWriterAgentExecutor.kt` for detailed implementation guidance.
+
+## Learn More
+
+- [Koog GitHub Repository](https://github.com/JetBrains/koog)
+- [Koog Documentation](https://koog.ai/)
diff --git a/samples/java/koog/build.gradle.kts b/samples/java/koog/build.gradle.kts
new file mode 100644
index 00000000..15fb8683
--- /dev/null
+++ b/samples/java/koog/build.gradle.kts
@@ -0,0 +1,43 @@
+plugins {
+ alias(libs.plugins.kotlin.jvm)
+ alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.ktlint)
+}
+
+dependencies {
+ implementation(platform(libs.kotlin.bom))
+ implementation(platform(libs.kotlinx.coroutines.bom))
+
+ implementation(libs.koog.agents)
+ implementation(libs.koog.agents.features.a2a.server)
+ implementation(libs.koog.agents.features.a2a.client)
+ implementation(libs.koog.a2a.transport.server.jsonrpc.http)
+ implementation(libs.koog.a2a.transport.client.jsonrpc.http)
+
+ implementation(libs.kotlinx.datetime)
+ implementation(libs.kotlinx.coroutines.core)
+
+ implementation(libs.ktor.server.cio)
+
+ runtimeOnly(libs.logback.classic)
+}
+
+fun registerRunExampleTask(
+ name: String,
+ mainClassName: String,
+) = tasks.register(name) {
+ doFirst {
+ standardInput = System.`in`
+ standardOutput = System.out
+ }
+
+ mainClass.set(mainClassName)
+ classpath = sourceSets["main"].runtimeClasspath
+}
+// Simple joke generation
+registerRunExampleTask("runExampleSimpleJokeServer", "ai.koog.example.simplejoke.ServerKt")
+registerRunExampleTask("runExampleSimpleJokeClient", "ai.koog.example.simplejoke.ClientKt")
+
+// Advanced joke generation
+registerRunExampleTask("runExampleAdvancedJokeServer", "ai.koog.example.advancedjoke.ServerKt")
+registerRunExampleTask("runExampleAdvancedJokeClient", "ai.koog.example.advancedjoke.ClientKt")
diff --git a/samples/java/koog/gradle.properties b/samples/java/koog/gradle.properties
new file mode 100644
index 00000000..4db6c1cf
--- /dev/null
+++ b/samples/java/koog/gradle.properties
@@ -0,0 +1,7 @@
+#Kotlin
+kotlin.code.style=official
+
+#Gradle
+org.gradle.jvmargs=-Dfile.encoding=UTF-8
+org.gradle.parallel=true
+org.gradle.caching=true
diff --git a/samples/java/koog/gradle/libs.versions.toml b/samples/java/koog/gradle/libs.versions.toml
new file mode 100644
index 00000000..1150725e
--- /dev/null
+++ b/samples/java/koog/gradle/libs.versions.toml
@@ -0,0 +1,29 @@
+[versions]
+kotlin = "2.2.20"
+kotlinx-coroutines = "1.10.2"
+kotlinx-datetime = "0.6.2"
+kotlinx-serialization = "1.8.1"
+ktor3 = "3.2.2"
+koog = "0.5.0"
+logback = "1.5.13"
+oshai-logging = "7.0.7"
+ktlint = "13.1.0"
+
+[libraries]
+kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" }
+kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom" }
+kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" }
+kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
+ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor3" }
+koog-agents = { module = "ai.koog:koog-agents", version.ref = "koog" }
+koog-agents-features-a2a-server = { module = "ai.koog:agents-features-a2a-server", version.ref = "koog" }
+koog-agents-features-a2a-client = { module = "ai.koog:agents-features-a2a-client", version.ref = "koog" }
+koog-a2a-transport-server-jsonrpc-http = { module = "ai.koog:a2a-transport-server-jsonrpc-http", version.ref = "koog" }
+koog-a2a-transport-client-jsonrpc-http = { module = "ai.koog:a2a-transport-client-jsonrpc-http", version.ref = "koog" }
+logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
+oshai-kotlin-logging = { module = "io.github.oshai:kotlin-logging", version.ref = "oshai-logging" }
+
+[plugins]
+kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
+kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
+ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
diff --git a/samples/java/koog/gradle/wrapper/gradle-wrapper.jar b/samples/java/koog/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846
GIT binary patch
literal 43764
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