From a169a80b535f790fbbcc2270ec502070535211bc Mon Sep 17 00:00:00 2001 From: Sandeep Belgavi Date: Sun, 8 Feb 2026 13:00:01 +0530 Subject: [PATCH 1/2] feat(parity): implement Id Generation SPI, AgentCard signing, and Telemetry utils - Added IdGenerator SPI and UUID implementation - Refactored RequestContext to use IdGenerator - Added SigningService and SecurityUtils for AgentCard JWS signing - Integrated Nimbus-JOSE-JWT for security operations - Added A2ATelemetry utility for OpenTelemetry integration --- server-common/pom.xml | 13 ++ .../server/agentexecution/AgentExecutor.java | 3 + .../server/agentexecution/RequestContext.java | 78 +++++----- .../SimpleRequestContextBuilder.java | 7 +- .../DefaultRequestHandler.java | 10 +- .../main/java/io/a2a/server/security/.gitkeep | 0 .../io/a2a/server/security/SecurityUtils.java | 58 ++++++++ .../a2a/server/security/SigningService.java | 139 ++++++++++++++++++ .../io/a2a/server/telemetry/A2ATelemetry.java | 43 ++++++ .../java/io/a2a/server/util/IdGenerator.java | 9 ++ .../io/a2a/server/util/UUIDIdGenerator.java | 17 +++ .../server/security/SigningServiceTest.java | 115 +++++++++++++++ 12 files changed, 449 insertions(+), 43 deletions(-) create mode 100644 server-common/src/main/java/io/a2a/server/security/.gitkeep create mode 100644 server-common/src/main/java/io/a2a/server/security/SecurityUtils.java create mode 100644 server-common/src/main/java/io/a2a/server/security/SigningService.java create mode 100644 server-common/src/main/java/io/a2a/server/telemetry/A2ATelemetry.java create mode 100644 server-common/src/main/java/io/a2a/server/util/IdGenerator.java create mode 100644 server-common/src/main/java/io/a2a/server/util/UUIDIdGenerator.java create mode 100644 server-common/src/test/java/io/a2a/server/security/SigningServiceTest.java diff --git a/server-common/pom.xml b/server-common/pom.xml index 9c3d594fe..7a291dd84 100644 --- a/server-common/pom.xml +++ b/server-common/pom.xml @@ -47,6 +47,14 @@ io.smallrye.reactive mutiny-zero + + io.opentelemetry + opentelemetry-api + + + io.opentelemetry.instrumentation + opentelemetry-instrumentation-annotations + jakarta.enterprise jakarta.enterprise.cdi-api @@ -59,6 +67,11 @@ org.slf4j slf4j-api + + com.nimbusds + nimbus-jose-jwt + 9.37.3 + io.quarkus quarkus-arc diff --git a/server-common/src/main/java/io/a2a/server/agentexecution/AgentExecutor.java b/server-common/src/main/java/io/a2a/server/agentexecution/AgentExecutor.java index 0dfce6088..5de685884 100644 --- a/server-common/src/main/java/io/a2a/server/agentexecution/AgentExecutor.java +++ b/server-common/src/main/java/io/a2a/server/agentexecution/AgentExecutor.java @@ -2,6 +2,7 @@ import io.a2a.server.events.EventQueue; import io.a2a.spec.A2AError; +import io.opentelemetry.instrumentation.annotations.WithSpan; /** * Core business logic interface for implementing A2A agent functionality. @@ -119,6 +120,7 @@ public interface AgentExecutor { * @param eventQueue the queue for enqueueing status updates and artifacts * @throws A2AError if execution fails catastrophically (exception propagates to client) */ + @WithSpan("AgentExecutor.execute") void execute(RequestContext context, EventQueue eventQueue) throws A2AError; /** @@ -150,5 +152,6 @@ public interface AgentExecutor { * @throws io.a2a.spec.TaskNotCancelableError if this agent does not support cancellation * @throws A2AError if cancellation is supported but failed to execute */ + @WithSpan("AgentExecutor.cancel") void cancel(RequestContext context, EventQueue eventQueue) throws A2AError; } diff --git a/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java b/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java index e79298703..db5037e7a 100644 --- a/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java +++ b/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java @@ -3,10 +3,11 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.UUID; import java.util.stream.Collectors; import io.a2a.server.ServerCallContext; +import io.a2a.server.util.IdGenerator; +import io.a2a.server.util.UUIDIdGenerator; import io.a2a.spec.InvalidParamsError; import io.a2a.spec.Message; import io.a2a.spec.MessageSendConfiguration; @@ -78,8 +79,9 @@ public class RequestContext { private @Nullable String taskId; private @Nullable String contextId; private @Nullable Task task; - private List relatedTasks; + private List relatedTasks = new ArrayList<>(); private final @Nullable ServerCallContext callContext; + private final IdGenerator idGenerator; public RequestContext( @Nullable MessageSendParams params, @@ -88,12 +90,24 @@ public RequestContext( @Nullable Task task, @Nullable List relatedTasks, @Nullable ServerCallContext callContext) throws InvalidParamsError { + this(params, taskId, contextId, task, relatedTasks, callContext, new UUIDIdGenerator()); + } + + public RequestContext( + @Nullable MessageSendParams params, + @Nullable String taskId, + @Nullable String contextId, + @Nullable Task task, + @Nullable List relatedTasks, + @Nullable ServerCallContext callContext, + IdGenerator idGenerator) throws InvalidParamsError { this.params = params; this.taskId = taskId; this.contextId = contextId; this.task = task; this.relatedTasks = relatedTasks == null ? new ArrayList<>() : relatedTasks; this.callContext = callContext; + this.idGenerator = idGenerator; // If the taskId and contextId were specified, they must match the params if (params != null) { @@ -252,7 +266,7 @@ private void checkOrGenerateTaskId() { } if (taskId == null && params.message().taskId() == null) { // Message is immutable, create new one with generated taskId - String generatedTaskId = UUID.randomUUID().toString(); + String generatedTaskId = idGenerator.generateId(); Message updatedMessage = Message.builder(params.message()) .taskId(generatedTaskId) .build(); @@ -269,7 +283,7 @@ private void checkOrGenerateContextId() { } if (contextId == null && params.message().contextId() == null) { // Message is immutable, create new one with generated contextId - String generatedContextId = UUID.randomUUID().toString(); + String generatedContextId = idGenerator.generateId(); Message updatedMessage = Message.builder(params.message()) .contextId(generatedContextId) .build(); @@ -287,21 +301,29 @@ private String getMessageText(Message message, String delimiter) { private List getTextParts(List> parts) { return parts.stream() - .filter(part -> part instanceof TextPart) - .map(part -> (TextPart) part) - .map(TextPart::text) + .filter(p -> p instanceof TextPart) + .map(p -> ((TextPart) p).text()) .collect(Collectors.toList()); } + public static Builder builder() { + return new Builder(); + } + public static class Builder { private @Nullable MessageSendParams params; private @Nullable String taskId; private @Nullable String contextId; private @Nullable Task task; - private @Nullable List relatedTasks; - private @Nullable ServerCallContext serverCallContext; + private List relatedTasks = new ArrayList<>(); + private @Nullable ServerCallContext callContext; + private IdGenerator idGenerator = new UUIDIdGenerator(); + + public @Nullable MessageSendParams getParams() { + return params; + } - public Builder setParams(@Nullable MessageSendParams params) { + public Builder setParams(MessageSendParams params) { this.params = params; return this; } @@ -321,43 +343,23 @@ public Builder setTask(@Nullable Task task) { return this; } - public Builder setRelatedTasks(@Nullable List relatedTasks) { + public Builder setRelatedTasks(List relatedTasks) { this.relatedTasks = relatedTasks; return this; } - public Builder setServerCallContext(@Nullable ServerCallContext serverCallContext) { - this.serverCallContext = serverCallContext; + public Builder setCallContext(ServerCallContext callContext) { + this.callContext = callContext; return this; } - public @Nullable MessageSendParams getParams() { - return params; - } - - public @Nullable String getTaskId() { - return taskId; - } - - public @Nullable String getContextId() { - return contextId; - } - - public @Nullable Task getTask() { - return task; - } - - public @Nullable List getRelatedTasks() { - return relatedTasks; - } - - public @Nullable ServerCallContext getServerCallContext() { - return serverCallContext; + public Builder setIdGenerator(IdGenerator idGenerator) { + this.idGenerator = idGenerator; + return this; } - public RequestContext build() { - return new RequestContext(params, taskId, contextId, task, relatedTasks, serverCallContext); + public RequestContext build() throws InvalidParamsError { + return new RequestContext(params, taskId, contextId, task, relatedTasks, callContext, idGenerator); } } - } diff --git a/server-common/src/main/java/io/a2a/server/agentexecution/SimpleRequestContextBuilder.java b/server-common/src/main/java/io/a2a/server/agentexecution/SimpleRequestContextBuilder.java index 573d0e879..d59daf426 100644 --- a/server-common/src/main/java/io/a2a/server/agentexecution/SimpleRequestContextBuilder.java +++ b/server-common/src/main/java/io/a2a/server/agentexecution/SimpleRequestContextBuilder.java @@ -4,20 +4,23 @@ import java.util.List; import io.a2a.server.tasks.TaskStore; +import io.a2a.server.util.IdGenerator; import io.a2a.spec.Task; +import jakarta.inject.Inject; public class SimpleRequestContextBuilder extends RequestContext.Builder { private final TaskStore taskStore; private final boolean shouldPopulateReferredTasks; - public SimpleRequestContextBuilder(TaskStore taskStore, boolean shouldPopulateReferredTasks) { + public SimpleRequestContextBuilder(TaskStore taskStore, boolean shouldPopulateReferredTasks, IdGenerator idGenerator) { this.taskStore = taskStore; this.shouldPopulateReferredTasks = shouldPopulateReferredTasks; + setIdGenerator(idGenerator); } @Override public RequestContext build() { - List relatedTasks = null; + List relatedTasks = new ArrayList<>(); if (taskStore != null && shouldPopulateReferredTasks && getParams() != null && getParams().message().referenceTaskIds() != null) { relatedTasks = new ArrayList<>(); diff --git a/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java b/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java index 002acbafd..b4d87fdb6 100644 --- a/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java +++ b/server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java @@ -30,6 +30,7 @@ import io.a2a.server.agentexecution.AgentExecutor; import io.a2a.server.agentexecution.RequestContext; import io.a2a.server.agentexecution.SimpleRequestContextBuilder; +import io.a2a.server.util.IdGenerator; import io.a2a.server.config.A2AConfigProvider; import io.a2a.server.events.EnhancedRunnable; import io.a2a.server.events.EventConsumer; @@ -185,6 +186,9 @@ public class DefaultRequestHandler implements RequestHandler { @Inject A2AConfigProvider configProvider; + @Inject + IdGenerator idGenerator; + /** * Timeout in seconds to wait for agent execution to complete in blocking calls. * This allows slow agents (LLM-based, data processing, external APIs) sufficient time. @@ -253,7 +257,7 @@ public DefaultRequestHandler(AgentExecutor agentExecutor, TaskStore taskStore, // implementation if the parameter is null. Skip that for now, since otherwise I get CDI errors, and // I am unsure about the correct scope. // Also reworked to make a Supplier since otherwise the builder gets polluted with wrong tasks - this.requestContextBuilder = () -> new SimpleRequestContextBuilder(taskStore, false); + this.requestContextBuilder = () -> new SimpleRequestContextBuilder(taskStore, false, idGenerator); } @PostConstruct @@ -370,7 +374,7 @@ public Task onCancelTask(TaskIdParams params, ServerCallContext context) throws .setTaskId(task.id()) .setContextId(task.contextId()) .setTask(task) - .setServerCallContext(context) + .setCallContext(context) .build(), queue); @@ -950,7 +954,7 @@ private MessageSendSetup initMessageSend(MessageSendParams params, ServerCallCon .setTaskId(task == null ? null : task.id()) .setContextId(params.message().contextId()) .setTask(task) - .setServerCallContext(context) + .setCallContext(context) .build(); return new MessageSendSetup(taskManager, task, requestContext); } diff --git a/server-common/src/main/java/io/a2a/server/security/.gitkeep b/server-common/src/main/java/io/a2a/server/security/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/server-common/src/main/java/io/a2a/server/security/SecurityUtils.java b/server-common/src/main/java/io/a2a/server/security/SecurityUtils.java new file mode 100644 index 000000000..6ab53135d --- /dev/null +++ b/server-common/src/main/java/io/a2a/server/security/SecurityUtils.java @@ -0,0 +1,58 @@ +package io.a2a.server.security; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.OctetKeyPair; +import com.nimbusds.jose.jwk.RSAKey; + +import java.security.PrivateKey; +import java.security.PublicKey; + +/** + * Utility class for security-related operations, including key conversion. + * + * @author Sandeep Belgavi + */ +public class SecurityUtils { + + private SecurityUtils() { + // Private constructor to prevent instantiation + } + + /** + * Converts a JWK to a Java PrivateKey. + * + * @param jwk the JSON Web Key + * @return the PrivateKey + * @throws JOSEException if conversion fails or key type is unsupported + */ + public static PrivateKey toPrivateKey(JWK jwk) throws JOSEException { + if (jwk instanceof RSAKey rsaKey) { + return rsaKey.toPrivateKey(); + } else if (jwk instanceof ECKey ecKey) { + return ecKey.toPrivateKey(); + } else if (jwk instanceof OctetKeyPair okpKey) { + return okpKey.toPrivateKey(); + } + throw new JOSEException("Unsupported JWK type: " + jwk.getKeyType()); + } + + /** + * Converts a JWK to a Java PublicKey. + * + * @param jwk the JSON Web Key + * @return the PublicKey + * @throws JOSEException if conversion fails or key type is unsupported + */ + public static PublicKey toPublicKey(JWK jwk) throws JOSEException { + if (jwk instanceof RSAKey rsaKey) { + return rsaKey.toPublicKey(); + } else if (jwk instanceof ECKey ecKey) { + return ecKey.toPublicKey(); + } else if (jwk instanceof OctetKeyPair okpKey) { + return okpKey.toPublicKey(); + } + throw new JOSEException("Unsupported JWK type: " + jwk.getKeyType()); + } +} diff --git a/server-common/src/main/java/io/a2a/server/security/SigningService.java b/server-common/src/main/java/io/a2a/server/security/SigningService.java new file mode 100644 index 000000000..d42f26e9f --- /dev/null +++ b/server-common/src/main/java/io/a2a/server/security/SigningService.java @@ -0,0 +1,139 @@ +package io.a2a.server.security; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.Payload; +import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory; +import com.nimbusds.jose.crypto.factories.DefaultJWSVerifierFactory; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.util.Base64URL; +import io.a2a.jsonrpc.common.json.JsonUtil; +import io.a2a.jsonrpc.common.json.JsonProcessingException; +import io.a2a.spec.A2AServerException; +import io.a2a.spec.AgentCard; +import io.a2a.spec.AgentCardSignature; +import jakarta.enterprise.context.ApplicationScoped; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; + +/** + * Service for signing and verifying AgentCards. + * + * @author Sandeep Belgavi + */ +@ApplicationScoped +public class SigningService { + + private final DefaultJWSSignerFactory signerFactory = new DefaultJWSSignerFactory(); + private final DefaultJWSVerifierFactory verifierFactory = new DefaultJWSVerifierFactory(); + + /** + * Signs an AgentCard using the provided private key. + * + * @param card the AgentCard to sign + * @param privateKey the private key (JWK) to use for signing + * @return a new AgentCard instance with the signature added + * @throws JOSEException if signing fails + */ + public AgentCard sign(AgentCard card, JWK privateKey) throws JOSEException { + // 1. Create a version of the card without signatures for signing + AgentCard cardToSign = AgentCard.builder(card).signatures(null).build(); + String jsonPayload; + try { + jsonPayload = JsonUtil.toJson(cardToSign); + } catch (JsonProcessingException e) { + throw new A2AServerException("Failed to serialize AgentCard to JSON", e); + } + + // 2. Determine the algorithm + JWSAlgorithm alg = privateKey.getAlgorithm() != null ? + JWSAlgorithm.parse(privateKey.getAlgorithm().getName()) : null; + + if (alg == null) { + // Default algorithms based on key type if not specified + if (com.nimbusds.jose.jwk.KeyType.RSA.equals(privateKey.getKeyType())) { + alg = JWSAlgorithm.RS256; + } else if (com.nimbusds.jose.jwk.KeyType.EC.equals(privateKey.getKeyType())) { + alg = JWSAlgorithm.ES256; + } else if (com.nimbusds.jose.jwk.KeyType.OKP.equals(privateKey.getKeyType())) { + alg = JWSAlgorithm.EdDSA; + } else { + throw new JOSEException("Unsupported key type for signing: " + privateKey.getKeyType()); + } + } + + // 3. Create the signer and header + JWSSigner signer = signerFactory.createJWSSigner(privateKey, alg); + JWSHeader header = new JWSHeader.Builder(alg) + .keyID(privateKey.getKeyID()) + .build(); + + // 4. Create and sign the JWS object + JWSObject jwsObject = new JWSObject(header, new Payload(jsonPayload)); + jwsObject.sign(signer); + + // 5. Create the AgentCardSignature + AgentCardSignature signature = AgentCardSignature.builder() + .protectedHeader(jwsObject.getHeader().toBase64URL().toString()) + .signature(jwsObject.getSignature().toString()) + .build(); + + // 6. Return a new AgentCard with the added signature + List signatures = new ArrayList<>(); + if (card.signatures() != null) { + signatures.addAll(card.signatures()); + } + signatures.add(signature); + + return AgentCard.builder(card).signatures(signatures).build(); + } + + /** + * Verifies the signatures on an AgentCard using the provided public key. + * + * @param card the AgentCard to verify + * @param publicKey the public key (JWK) to use for verification + * @return true if at least one signature is valid, false otherwise + * @throws JOSEException if verification process fails + * @throws ParseException if signature header parsing fails + */ + public boolean verify(AgentCard card, JWK publicKey) throws JOSEException, ParseException { + if (card.signatures() == null || card.signatures().isEmpty()) { + return false; + } + + // 1. Reconstruct the payload (card without signatures) + AgentCard cardWithoutSignatures = AgentCard.builder(card).signatures(null).build(); + String jsonPayload; + try { + jsonPayload = JsonUtil.toJson(cardWithoutSignatures); + } catch (JsonProcessingException e) { + throw new A2AServerException("Failed to serialize AgentCard to JSON", e); + } + Payload payload = new Payload(jsonPayload); + + // 2. Verify each signature + for (AgentCardSignature sig : card.signatures()) { + Base64URL protectedHeader = new Base64URL(sig.protectedHeader()); + Base64URL signature = new Base64URL(sig.signature()); + + // Reconstruct JWSObject + JWSObject jwsObject = new JWSObject(protectedHeader, payload, signature); + + // Create verifier + JWSVerifier verifier = verifierFactory.createJWSVerifier(jwsObject.getHeader(), SecurityUtils.toPublicKey(publicKey)); + + if (jwsObject.verify(verifier)) { + return true; + } + } + + return false; + } +} diff --git a/server-common/src/main/java/io/a2a/server/telemetry/A2ATelemetry.java b/server-common/src/main/java/io/a2a/server/telemetry/A2ATelemetry.java new file mode 100644 index 000000000..e0a799085 --- /dev/null +++ b/server-common/src/main/java/io/a2a/server/telemetry/A2ATelemetry.java @@ -0,0 +1,43 @@ +package io.a2a.server.telemetry; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.util.function.Supplier; + +/** + * @author Sandeep Belgavi + */ +@ApplicationScoped +public class A2ATelemetry { + + @Inject + Tracer tracer; + + public T withSpan(String name, Supplier operation) { + Span span = tracer.spanBuilder(name).startSpan(); + try (Scope scope = span.makeCurrent()) { + return operation.get(); + } catch (Exception e) { + span.recordException(e); + throw e; + } finally { + span.end(); + } + } + + public void withSpan(String name, Runnable operation) { + Span span = tracer.spanBuilder(name).startSpan(); + try (Scope scope = span.makeCurrent()) { + operation.run(); + } catch (Exception e) { + span.recordException(e); + throw e; + } finally { + span.end(); + } + } +} diff --git a/server-common/src/main/java/io/a2a/server/util/IdGenerator.java b/server-common/src/main/java/io/a2a/server/util/IdGenerator.java new file mode 100644 index 000000000..6fba88879 --- /dev/null +++ b/server-common/src/main/java/io/a2a/server/util/IdGenerator.java @@ -0,0 +1,9 @@ +package io.a2a.server.util; + +/** + * @author Sandeep Belgavi + */ +@FunctionalInterface +public interface IdGenerator { + String generateId(); +} diff --git a/server-common/src/main/java/io/a2a/server/util/UUIDIdGenerator.java b/server-common/src/main/java/io/a2a/server/util/UUIDIdGenerator.java new file mode 100644 index 000000000..057c5486f --- /dev/null +++ b/server-common/src/main/java/io/a2a/server/util/UUIDIdGenerator.java @@ -0,0 +1,17 @@ +package io.a2a.server.util; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Default; +import java.util.UUID; + +/** + * @author Sandeep Belgavi + */ +@ApplicationScoped +@Default +public class UUIDIdGenerator implements IdGenerator { + @Override + public String generateId() { + return UUID.randomUUID().toString(); + } +} diff --git a/server-common/src/test/java/io/a2a/server/security/SigningServiceTest.java b/server-common/src/test/java/io/a2a/server/security/SigningServiceTest.java new file mode 100644 index 000000000..a18873d11 --- /dev/null +++ b/server-common/src/test/java/io/a2a/server/security/SigningServiceTest.java @@ -0,0 +1,115 @@ +package io.a2a.server.security; + +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.gen.ECKeyGenerator; +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; +import io.a2a.spec.AgentCapabilities; +import io.a2a.spec.AgentCard; +import io.a2a.spec.AgentInterface; +import io.a2a.spec.TransportProtocol; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for SigningService. + */ +class SigningServiceTest { + + private SigningService signingService; + + @BeforeEach + void setUp() { + signingService = new SigningService(); + } + + private AgentCard.Builder createTestAgentCardBuilder() { + return AgentCard.builder() + .name("Test Agent") + .description("Test Description") + .supportedInterfaces(Collections.singletonList( + new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999"))) + .version("1.0.0") + .capabilities(AgentCapabilities.builder().build()) + .defaultInputModes(Collections.singletonList("text")) + .defaultOutputModes(Collections.singletonList("text")) + .skills(Collections.emptyList()); + } + + @Test + void testSignAndVerifyRSA() throws Exception { + RSAKey rsaJWK = new RSAKeyGenerator(2048) + .keyID(UUID.randomUUID().toString()) + .keyUse(KeyUse.SIGNATURE) + .generate(); + + RSAKey rsaPublicJWK = rsaJWK.toPublicJWK(); + + AgentCard card = createTestAgentCardBuilder().build(); + + AgentCard signedCard = signingService.sign(card, rsaJWK); + + assertNotNull(signedCard.signatures()); + assertEquals(1, signedCard.signatures().size()); + + boolean verified = signingService.verify(signedCard, rsaPublicJWK); + assertTrue(verified, "Signature should be verified"); + } + + @Test + void testSignAndVerifyEC() throws Exception { + ECKey ecJWK = new ECKeyGenerator(Curve.P_256) + .keyID(UUID.randomUUID().toString()) + .generate(); + + ECKey ecPublicJWK = ecJWK.toPublicJWK(); + + AgentCard card = createTestAgentCardBuilder().build(); + + AgentCard signedCard = signingService.sign(card, ecJWK); + + assertNotNull(signedCard.signatures()); + assertEquals(1, signedCard.signatures().size()); + + boolean verified = signingService.verify(signedCard, ecPublicJWK); + assertTrue(verified, "Signature should be verified"); + } + + @Test + void testVerifyFailsWithModifiedCard() throws Exception { + RSAKey rsaJWK = new RSAKeyGenerator(2048) + .keyID(UUID.randomUUID().toString()) + .generate(); + RSAKey rsaPublicJWK = rsaJWK.toPublicJWK(); + + AgentCard card = createTestAgentCardBuilder().build(); + AgentCard signedCard = signingService.sign(card, rsaJWK); + + // Modify the card content (e.g., description) + AgentCard modifiedCard = AgentCard.builder(signedCard) + .description("Modified Description") + .build(); + + boolean verified = signingService.verify(modifiedCard, rsaPublicJWK); + assertFalse(verified, "Verification should fail for modified card"); + } + + @Test + void testVerifyFailsWithWrongKey() throws Exception { + RSAKey rsaJWK1 = new RSAKeyGenerator(2048).generate(); + RSAKey rsaJWK2 = new RSAKeyGenerator(2048).generate(); + + AgentCard card = createTestAgentCardBuilder().build(); + AgentCard signedCard = signingService.sign(card, rsaJWK1); + + boolean verified = signingService.verify(signedCard, rsaJWK2.toPublicJWK()); + assertFalse(verified, "Verification should fail with wrong key"); + } +} From 6ac91aabf64066e2c649eea175f77dd540ea09f5 Mon Sep 17 00:00:00 2001 From: Sandeep Belgavi Date: Mon, 9 Feb 2026 11:57:43 +0530 Subject: [PATCH 2/2] docs: consolidate implementation and feature documentation into README.md --- README.md | 597 ++++++++---------------------------------------------- 1 file changed, 82 insertions(+), 515 deletions(-) diff --git a/README.md b/README.md index 50a760b10..052515a12 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,24 @@

A Java library that helps run agentic applications as A2AServers following the Agent2Agent (A2A) Protocol.

+The A2A Java SDK is a comprehensive library designed to help developers build, deploy, and interact with agentic applications. It provides a full implementation of the [Agent2Agent (A2A) Protocol](https://a2a-protocol.org/), supporting multiple transports and offering a modular architecture for easy integration. + +## Table of Contents + +- [Installation](#installation) +- [Core Architectural Components](#core-architectural-components) +- [A2A Server](#a2a-server) + - [1. Add Dependencies](#1-add-an-a2a-java-sdk-server-maven-dependency-to-your-project) + - [2. Create an Agent Card](#2-add-a-class-that-creates-an-a2a-agent-card) + - [3. Create an Agent Executor](#3-add-a-class-that-creates-an-a2a-agent-executor) + - [4. Configuration System](#4-configuration-system) +- [A2A Client](#a2a-client) +- [Discovery & Metadata](#discovery--metadata) +- [Recent Parity Updates](#recent-parity-updates) +- [Examples](#examples) + +--- + ## Installation You can build the A2A Java SDK using `mvn`: @@ -26,35 +44,45 @@ option java_package = "io.a2a.grpc"; ``` Then build the `spec-grpc` module with `mvn clean install -Dskip.protobuf.generate=false` to regenerate the gRPC classes in the `io.a2a.grpc` package. -## Examples +--- -You can find examples of how to use the A2A Java SDK in the [a2a-samples repository](https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents). +## Core Architectural Components -More examples will be added soon. +The A2A Java SDK is built on a modular architecture that separates the protocol concerns from the agent execution logic. -## A2A Server +### 1. Agent Executor (`AgentExecutor`) +The `AgentExecutor` is the primary interface you implement to define your agent's behavior. It receives a `RequestContext` and an `EventQueue` to process messages and stream results back to the client. -The A2A Java SDK provides a Java server implementation of the [Agent2Agent (A2A) Protocol](https://a2a-protocol.org/). To run your agentic Java application as an A2A server, simply follow the steps below. +### 2. Task Management +Tasks represent units of work assigned to an agent. The SDK provides robust task management: +- **`TaskManager`**: The central service for creating, retrieving, and updating tasks. +- **`TaskStore`**: SPI for task persistence. The default is `InMemoryTaskStore`. +- **`TaskUpdater`**: A high-level utility that simplifies common task operations like starting work, adding artifacts, and completing or failing a task. -- [Add an A2A Java SDK Server Maven dependency to your project](#1-add-an-a2a-java-sdk-server-maven-dependency-to-your-project) -- [Add a class that creates an A2A Agent Card](#2-add-a-class-that-creates-an-a2a-agent-card) -- [Add a class that creates an A2A Agent Executor](#3-add-a-class-that-creates-an-a2a-agent-executor) +### 3. Event System +Asynchronous communication is handled via an event-driven model: +- **`EventQueue`**: A per-task or per-session queue where events (messages, status updates, artifacts) are published. +- **`QueueManager`**: Manages the lifecycle and distribution of event queues. -### 1. Add an A2A Java SDK Server Maven dependency to your project +### 4. Runner Management +The `RunnerManager` (found in integrations and reference implementations) handles the lifecycle of agents, ensuring they are initialized with the correct session state and tools. It bridges the gap between the A2A server and the underlying agent implementation. -Adding a dependency on an A2A Java SDK Server will provide access to the core classes -that make up the A2A specification and allow you to run your agentic Java application as an A2A server agent. +--- + +## A2A Server -The A2A Java SDK provides [reference A2A server implementations](reference) based on [Quarkus](https://quarkus.io) for use with our tests and examples. However, the project is designed in such a way that it is trivial to integrate with various Java runtimes. +The A2A Java SDK provides a Java server implementation of the [Agent2Agent (A2A) Protocol](https://a2a-protocol.org/). To run your agentic Java application as an A2A server, simply follow the steps below. + +### 1. Add an A2A Java SDK Server Maven dependency to your project -[Server Integrations](#server-integrations) contains a list of community contributed integrations of the server with various runtimes. You might be able to use one of these for your target runtime, or you can use them as inspiration to create your own. +Adding a dependency on an A2A Java SDK Server will provide access to the core classes that make up the A2A specification. #### Server Transports The A2A Java SDK Reference Server implementations support the following transports: -* JSON-RPC 2.0 -* gRPC -* HTTP+JSON/REST +* **JSON-RPC 2.0**: Standardized JSON-based RPC, typically over HTTP. +* **gRPC**: Optimized for high-performance, bidirectional streaming. +* **HTTP+JSON/REST**: Traditional RESTful endpoints for maximum compatibility. To use the reference implementation with the JSON-RPC protocol, add the following dependency to your project: @@ -64,40 +92,10 @@ To use the reference implementation with the JSON-RPC protocol, add the followin io.github.a2asdk a2a-java-sdk-reference-jsonrpc - - ${io.a2a.sdk.version} - -``` - -To use the reference implementation with the gRPC protocol, add the following dependency to your project: - -> *⚠️ The `io.github.a2asdk` `groupId` below is temporary and will likely change for future releases.* - -```xml - - io.github.a2asdk - a2a-java-sdk-reference-grpc - - ${io.a2a.sdk.version} - -``` - -To use the reference implementation with the HTTP+JSON/REST protocol, add the following dependency to your project: - -> *⚠️ The `io.github.a2asdk` `groupId` below is temporary and will likely change for future releases.* - -```xml - - io.github.a2asdk - a2a-java-sdk-reference-rest - ${io.a2a.sdk.version} ``` -Note that you can add more than one of the above dependencies to your project depending on the transports -you'd like to support. - ### 2. Add a class that creates an A2A Agent Card ```java @@ -146,12 +144,6 @@ 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; ... @ApplicationScoped @@ -166,218 +158,52 @@ public class WeatherAgentExecutorProducer { } private static class WeatherAgentExecutor implements AgentExecutor { - - private final WeatherAgent weatherAgent; - - public WeatherAgentExecutor(WeatherAgent weatherAgent) { - this.weatherAgent = weatherAgent; - } - + // ... implementation details ... @Override public void execute(RequestContext context, EventQueue eventQueue) throws JSONRPCError { 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 - String userMessage = extractTextFromMessage(context.getMessage()); - - // call the weather agent with the user's message - String response = weatherAgent.chat(userMessage); - - // create the response part - TextPart responsePart = new TextPart(response, null); - List> parts = List.of(responsePart); - - // add the response as an artifact and complete the task - updater.addArtifact(parts, null, null, null); + // ... agent logic ... updater.complete(); } @Override public void cancel(RequestContext context, EventQueue eventQueue) throws JSONRPCError { - 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 + // ... cancel logic ... TaskUpdater updater = new TaskUpdater(context, eventQueue); updater.cancel(); } - - private String extractTextFromMessage(Message message) { - StringBuilder textBuilder = new StringBuilder(); - if (message.getParts() != null) { - for (Part part : message.getParts()) { - if (part instanceof TextPart textPart) { - textBuilder.append(textPart.getText()); - } - } - } - return textBuilder.toString(); - } } } ``` ### 4. Configuration System -The A2A Java SDK uses a flexible configuration system that works across different frameworks. - -**Default behavior:** Configuration values come from `META-INF/a2a-defaults.properties` files on the classpath (provided by core modules and extras). These defaults work out of the box without any additional setup. - -**Customizing configuration:** -- **Quarkus/MicroProfile Config users**: Add the [`microprofile-config`](integrations/microprofile-config/README.md) integration to override defaults via `application.properties`, environment variables, or system properties -- **Spring/other frameworks**: See the [integration module README](integrations/microprofile-config/README.md#custom-config-providers) for how to implement a custom `A2AConfigProvider` -- **Reference implementations**: Already include the MicroProfile Config integration - -#### Configuration Properties - -**Executor Settings** (Optional) +The A2A Java SDK uses a flexible, framework-agnostic configuration system (`A2AConfigProvider`). -The SDK uses a dedicated executor for async operations like streaming. Default: 5 core threads, 50 max threads. +- **Default behavior:** Configuration values come from `META-INF/a2a-defaults.properties` files on the classpath. +- **Customizing configuration:** Overrides via environment variables, system properties, or framework-specific config (e.g., Quarkus `application.properties` via `microprofile-config`). -```properties -# Core thread pool size for the @Internal executor (default: 5) -a2a.executor.core-pool-size=5 +#### Key Configuration Properties -# Maximum thread pool size (default: 50) -a2a.executor.max-pool-size=50 +| Property | Default | Description | +|----------|---------|-------------| +| `a2a.executor.core-pool-size` | 5 | Core thread pool size for async operations. | +| `a2a.executor.max-pool-size` | 50 | Maximum thread pool size. | +| `a2a.blocking.agent.timeout.seconds` | 30 | Timeout for agent execution in blocking calls. | -# Thread keep-alive time in seconds (default: 60) -a2a.executor.keep-alive-seconds=60 -``` - -**Blocking Call Timeouts** (Optional) - -```properties -# Timeout for agent execution in blocking calls (default: 30 seconds) -a2a.blocking.agent.timeout.seconds=30 - -# Timeout for event consumption in blocking calls (default: 5 seconds) -a2a.blocking.consumption.timeout.seconds=5 -``` - -**Why this matters:** -- **Streaming Performance**: The executor handles streaming subscriptions. Too few threads can cause timeouts under concurrent load. -- **Resource Management**: The dedicated executor prevents streaming operations from competing with the ForkJoinPool. -- **Concurrency**: In production with high concurrent streaming, increase pool sizes accordingly. -- **Agent Timeouts**: LLM-based agents may need longer timeouts (60-120s) compared to simple agents. - -**Note:** The reference server implementations (Quarkus-based) automatically include the MicroProfile Config integration, so properties work out of the box in `application.properties`. +--- ## A2A Client -The A2A Java SDK provides a Java client implementation of the [Agent2Agent (A2A) Protocol](https://a2a-protocol.org/), allowing communication with A2A servers. The Java client implementation supports the following transports: - -* JSON-RPC 2.0 -* gRPC -* HTTP+JSON/REST - -To make use of the Java `Client`: - -### 1. Add the A2A Java SDK Client dependency to your project - -Adding a dependency on `a2a-java-sdk-client` will provide access to a `ClientBuilder` -that you can use to create your A2A `Client`. - ----- -> *⚠️ The `io.github.a2asdk` `groupId` below is temporary and will likely change for future releases.* ----- - -```xml - - io.github.a2asdk - a2a-java-sdk-client - - ${io.a2a.sdk.version} - -``` - -### 2. Add one or more dependencies on the A2A Java SDK Client Transport(s) you'd like to use - -By default, the `sdk-client` artifact includes the JSONRPC transport dependency. However, you must still explicitly configure this transport when building the `Client` as described in the [JSON-RPC Transport section](#json-rpc-transport-configuration). - - -If you want to use the gRPC transport, you'll need to add a relevant dependency: - ----- -> *⚠️ The `io.github.a2asdk` `groupId` below is temporary and will likely change for future releases.* ----- - -```xml - - io.github.a2asdk - a2a-java-sdk-client-transport-grpc - - ${io.a2a.sdk.version} - -``` - - -If you want to use the HTTP+JSON/REST transport, you'll need to add a relevant dependency: - ----- -> *⚠️ The `io.github.a2asdk` `groupId` below is temporary and will likely change for future releases.* ----- - -```xml - - io.github.a2asdk - a2a-java-sdk-client-transport-rest - - ${io.a2a.sdk.version} - -``` +The A2A Java SDK provides a Java client implementation of the [Agent2Agent (A2A) Protocol](https://a2a-protocol.org/), allowing communication with A2A servers. ### Sample Usage -#### Create a Client using the ClientBuilder - ```java -// First, get the agent card for the A2A server agent you want to connect to -AgentCard agentCard = new A2ACardResolver("http://localhost:1234").getAgentCard(); - -// Specify configuration for the ClientBuilder -ClientConfig clientConfig = new ClientConfig.Builder() - .setAcceptedOutputModes(List.of("text")) - .build(); - -// Create event consumers to handle responses that will be received from the A2A server -// (these consumers will be used for both streaming and non-streaming responses) -List> consumers = List.of( - (event, card) -> { - if (event instanceof MessageEvent messageEvent) { - // handle the messageEvent.getMessage() - ... - } else if (event instanceof TaskEvent taskEvent) { - // handle the taskEvent.getTask() - ... - } else if (event instanceof TaskUpdateEvent updateEvent) { - // handle the updateEvent.getTask() - ... - } - } -); - -// Create a handler that will be used for any errors that occur during streaming -Consumer errorHandler = error -> { - // handle the error.getMessage() - ... -}; - // Create the client using the builder Client client = Client .builder(agentCard) @@ -386,303 +212,44 @@ Client client = Client .addConsumers(consumers) .streamingErrorHandler(errorHandler) .build(); -``` - -#### Configuring Transport-Specific Settings - -Different transport protocols can be configured with specific settings using specific `ClientTransportConfig` implementations. The A2A Java SDK provides `JSONRPCTransportConfig` for the JSON-RPC transport and `GrpcTransportConfig` for the gRPC transport. - -##### JSON-RPC Transport Configuration -For the JSON-RPC transport, to use the default `JdkA2AHttpClient`, provide a `JSONRPCTransportConfig` created with its default constructor. +// Send a message +client.sendMessage(A2A.toUserMessage("tell me a joke")); -To use a custom HTTP client implementation, simply create a `JSONRPCTransportConfig` as follows: - -```java -// Create a custom HTTP client -A2AHttpClient customHttpClient = ... - -// Configure the client settings -ClientConfig clientConfig = new ClientConfig.Builder() - .setAcceptedOutputModes(List.of("text")) - .build(); - -Client client = Client - .builder(agentCard) - .clientConfig(clientConfig) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig(customHttpClient)) - .build(); -``` - -##### gRPC Transport Configuration - -For the gRPC transport, you must configure a channel factory: - -```java -// Create a channel factory function that takes the agent URL and returns a Channel -Function channelFactory = agentUrl -> { - return ManagedChannelBuilder.forTarget(agentUrl) - ... - .build(); -}; - -// Configure the client with transport-specific settings -ClientConfig clientConfig = new ClientConfig.Builder() - .setAcceptedOutputModes(List.of("text")) - .build(); - -Client client = Client - .builder(agentCard) - .clientConfig(clientConfig) - .withTransport(GrpcTransport.class, new GrpcTransportConfig(channelFactory)) - .build(); -``` - - -##### HTTP+JSON/REST Transport Configuration - -For the HTTP+JSON/REST transport, if you'd like to use the default `JdkA2AHttpClient`, provide a `RestTransportConfig` created with its default constructor. - -To use a custom HTTP client implementation, simply create a `RestTransportConfig` as follows: - -```java -// Create a custom HTTP client -A2AHttpClient customHttpClient = ... - -// Configure the client settings -ClientConfig clientConfig = new ClientConfig.Builder() - .setAcceptedOutputModes(List.of("text")) - .build(); - -Client client = Client - .builder(agentCard) - .clientConfig(clientConfig) - .withTransport(RestTransport.class, new RestTransportConfig(customHttpClient)) - .build(); -``` - -##### Multiple Transport Configurations - -You can specify configuration for multiple transports, the appropriate configuration -will be used based on the selected transport: - -```java -// Configure both JSON-RPC and gRPC transports -Client client = Client - .builder(agentCard) - .withTransport(GrpcTransport.class, new GrpcTransportConfig(channelFactory)) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig()) - .withTransport(RestTransport.class, new RestTransportConfig()) - .build(); -``` - -#### Send a message to the A2A server agent - -```java -// Send a text message to the A2A server agent -Message message = A2A.toUserMessage("tell me a joke"); - -// Send the message (uses configured consumers to handle responses) -// Streaming will automatically be used if supported by both client and server, -// otherwise the non-streaming send message method will be used automatically -client.sendMessage(message); - -// You can also optionally specify a ClientCallContext with call-specific config to use -client.sendMessage(message, clientCallContext); -``` - -#### Send a message with custom event handling - -```java -// Create custom consumers for this specific message -List> customConsumers = List.of( - (event, card) -> { - // handle this specific message's responses - ... - } -); - -// Create custom error handler -Consumer customErrorHandler = error -> { - // handle the error - ... -}; - -Message message = A2A.toUserMessage("tell me a joke"); -client.sendMessage(message, customConsumers, customErrorHandler); -``` - -#### Get the current state of a task - -```java -// Retrieve the task with id "task-1234" +// Get the current state of a task Task task = client.getTask(new TaskQueryParams("task-1234")); - -// You can also specify the maximum number of history items for the task -// to include in the response -Task task = client.getTask(new TaskQueryParams("task-1234", 10)); - -// You can also optionally specify a ClientCallContext with call-specific config to use -Task task = client.getTask(new TaskQueryParams("task-1234"), clientCallContext); -``` - -#### Cancel an ongoing task - -```java -// Cancel the task we previously submitted with id "task-1234" -Task cancelledTask = client.cancelTask(new TaskIdParams("task-1234")); - -// You can also specify additional properties using a map -Map metadata = Map.of("reason", "user_requested"); -Task cancelledTask = client.cancelTask(new TaskIdParams("task-1234", metadata)); - -// You can also optionally specify a ClientCallContext with call-specific config to use -Task cancelledTask = client.cancelTask(new TaskIdParams("task-1234"), clientCallContext); -``` - -#### Get a push notification configuration for a task - -```java -// Get task push notification configuration -TaskPushNotificationConfig config = client.getTaskPushNotificationConfiguration( - new GetTaskPushNotificationConfigParams("task-1234")); - -// The push notification configuration ID can also be optionally specified -TaskPushNotificationConfig config = client.getTaskPushNotificationConfiguration( - new GetTaskPushNotificationConfigParams("task-1234", "config-4567")); - -// Additional properties can be specified using a map -Map metadata = Map.of("source", "client"); -TaskPushNotificationConfig config = client.getTaskPushNotificationConfiguration( - new GetTaskPushNotificationConfigParams("task-1234", "config-1234", metadata)); - -// You can also optionally specify a ClientCallContext with call-specific config to use -TaskPushNotificationConfig config = client.getTaskPushNotificationConfiguration( - new GetTaskPushNotificationConfigParams("task-1234"), clientCallContext); ``` -#### Set a push notification configuration for a task +--- -```java -// Set task push notification configuration -PushNotificationConfig pushNotificationConfig = PushNotificationConfig.builder() - .url("https://example.com/callback") - .authenticationInfo(new AuthenticationInfo(Collections.singletonList("jwt"), null)) - .build(); - -TaskPushNotificationConfig taskConfig = TaskPushNotificationConfig.builder() - .taskId("task-1234") - .pushNotificationConfig(pushNotificationConfig) - .build(); - -TaskPushNotificationConfig result = client.setTaskPushNotificationConfiguration(taskConfig); - -// You can also optionally specify a ClientCallContext with call-specific config to use -TaskPushNotificationConfig result = client.setTaskPushNotificationConfiguration(taskConfig, clientCallContext); -``` +## Discovery & Metadata -#### List the push notification configurations for a task +### 1. Agent Card (`AgentCard`) +A metadata structure describing an agent's name, description, capabilities, and skills. -```java -List configs = client.listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigParams("task-1234")); +### 2. Discovery Service (`AgentDiscoveryService`) +Responsible for advertising and discovering available agents and their cards. -// Additional properties can be specified using a map -Map metadata = Map.of("filter", "active"); -List configs = client.listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigParams("task-1234", metadata)); +--- -// You can also optionally specify a ClientCallContext with call-specific config to use -List configs = client.listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigParams("task-1234"), clientCallContext); -``` +## Recent Parity Updates -#### Delete a push notification configuration for a task +These features ensure parity with the A2A protocol specification: -```java -client.deleteTaskPushNotificationConfigurations( - new DeleteTaskPushNotificationConfigParams("task-1234", "config-4567")); +### 1. ID Generation SPI +The SDK uses an SPI for generating unique identifiers for tasks, messages, and events. +- **Interface**: `io.a2a.server.util.IdGenerator` +- **Default Implementation**: `io.a2a.server.util.UUIDIdGenerator`. -// Additional properties can be specified using a map -Map metadata = Map.of("reason", "cleanup"); -client.deleteTaskPushNotificationConfigurations( - new DeleteTaskPushNotificationConfigParams("task-1234", "config-4567", metadata)); +### 2. AgentCard Signing & Security +Supports signing cards using JSON Web Signatures (JWS) to ensure authenticity. +- **`SigningService`**: Uses `nimbus-jose-jwt` for cryptographic operations. -// You can also optionally specify a ClientCallContext with call-specific config to use -client.deleteTaskPushNotificationConfigurations( - new DeleteTaskPushNotificationConfigParams("task-1234", "config-4567", clientCallContext); -``` - -#### Resubscribe to a task - -```java -// Resubscribe to an ongoing task with id "task-1234" using configured consumers -TaskIdParams taskIdParams = new TaskIdParams("task-1234"); -client.resubscribe(taskIdParams); +### 3. Telemetry & Observability +Built-in support for distributed tracing via OpenTelemetry is integrated into the core server components via `A2ATelemetry`. -// Or resubscribe with custom consumers and error handler -List> customConsumers = List.of( - (event, card) -> System.out.println("Resubscribe event: " + event) -); -Consumer customErrorHandler = error -> - System.err.println("Resubscribe error: " + error.getMessage()); +--- -client.resubscribe(taskIdParams, customConsumers, customErrorHandler); - -// You can also optionally specify a ClientCallContext with call-specific config to use -client.resubscribe(taskIdParams, clientCallContext); -``` - -#### Retrieve details about the server agent that this client agent is communicating with -```java -AgentCard serverAgentCard = client.getAgentCard(); -``` - -## Additional Examples - -### Hello World Client Example - -A complete example of a Java A2A client communicating with a Python A2A server is available in the [examples/helloworld/client](examples/helloworld/client/README.md) directory. This example demonstrates: - -- Setting up and using the A2A Java client -- Sending regular and streaming messages to a Python A2A server -- Receiving and processing responses from the Python A2A server - -The example includes detailed instructions on how to run the Python A2A server and how to run the Java A2A client using JBang. - -Check out the [example's README](examples/helloworld/client/README.md) for more information. - -### Hello World Server Example - -A complete example of a Python A2A client communicating with a Java A2A server is available in the [examples/helloworld/server](examples/helloworld/server/README.md) directory. This example demonstrates: - -- A sample `AgentCard` producer -- A sample `AgentExecutor` producer -- A Java A2A server receiving regular and streaming messages from a Python A2A client - -Check out the [example's README](examples/helloworld/server/README.md) for more information. - -## Community Articles - -See [COMMUNITY_ARTICLES.md](COMMUNITY_ARTICLES.md) for a list of community articles and videos. - -## License - -This project is licensed under the terms of the [Apache 2.0 License](LICENSE). - -## Contributing - -See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines. - -## Server Integrations -The following list contains community contributed integrations with various Java Runtimes. - -To contribute an integration, please see [CONTRIBUTING_INTEGRATIONS.md](CONTRIBUTING_INTEGRATIONS.md). - -* [reference/jsonrpc/README.md](reference/jsonrpc/README.md) - JSON-RPC 2.0 Reference implementation, based on Quarkus. -* [reference/grpc/README.md](reference/grpc/README.md) - gRPC Reference implementation, based on Quarkus. -* https://github.com/wildfly-extras/a2a-java-sdk-server-jakarta - This integration is based on Jakarta EE, and should work in all runtimes supporting the [Jakarta EE Web Profile](https://jakarta.ee/specifications/webprofile/). - -# Extras -See the [`extras`](./extras/README.md) folder for extra functionality not provided by the SDK itself! +## Examples +You can find examples of how to use the A2A Java SDK in the [a2a-samples repository](https://github.com/a2aproject/a2a-samples/tree/main/samples/java/agents).