Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@
@Blocking
public class QuarkusGrpcHandler extends GrpcHandler {

private final AgentCard agentCard;
private final AgentCard extendedAgentCard;
private final Instance<AgentCard> agentCard;
private final Instance<AgentCard> extendedAgentCard;
private final RequestHandler requestHandler;
private final Instance<CallContextFactory> callContextFactoryInstance;
private final Executor executor;
Expand Down Expand Up @@ -106,17 +106,13 @@ public class QuarkusGrpcHandler extends GrpcHandler {
* @param executor the executor for async operations (qualified with {@code @Internal})
*/
@Inject
public QuarkusGrpcHandler(@PublicAgentCard AgentCard agentCard,
public QuarkusGrpcHandler(@PublicAgentCard Instance<AgentCard> agentCard,
@ExtendedAgentCard Instance<AgentCard> extendedAgentCard,
RequestHandler requestHandler,
Instance<CallContextFactory> callContextFactoryInstance,
@Internal Executor executor) {
this.agentCard = agentCard;
if (extendedAgentCard != null && extendedAgentCard.isResolvable()) {
this.extendedAgentCard = extendedAgentCard.get();
} else {
this.extendedAgentCard = null;
}
this.extendedAgentCard = extendedAgentCard;
this.requestHandler = requestHandler;
this.callContextFactoryInstance = callContextFactoryInstance;
this.executor = executor;
Expand All @@ -129,12 +125,15 @@ protected RequestHandler getRequestHandler() {

@Override
protected AgentCard getAgentCard() {
return agentCard;
return agentCard.get();
}

@Override
protected AgentCard getExtendedAgentCard() {
return extendedAgentCard;
if (extendedAgentCard != null && extendedAgentCard.isResolvable()) {
return extendedAgentCard.get();
}
return null;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.a2aproject.sdk.server.grpc.quarkus;

import jakarta.enterprise.inject.Instance;

import org.a2aproject.sdk.server.ResolvedInstance;
import org.a2aproject.sdk.spec.AgentCard;
import org.a2aproject.sdk.transport.grpc.handler.CallContextFactory;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;

/**
* Verifies that {@link QuarkusGrpcHandler} does not eagerly resolve {@link Instance} parameters
* during construction. This guards against the regression described in
* <a href="https://github.com/a2aproject/a2a-java/issues/1033">issue #1033</a>, where eager
* resolution prevented startup when the AgentCard producer depended on the HTTP server address.
*/
class QuarkusGrpcHandlerLazyResolutionTest {

@Test
void constructorDoesNotResolveAgentCardInstances() {
Instance<AgentCard> throwOnGet = new ResolvedInstance<>(null) {
@Override
public AgentCard get() {
throw new AssertionError("Instance.get() must not be called during construction");
}

@Override
public boolean isUnsatisfied() {
return false;
}
};

@SuppressWarnings("unchecked")
Instance<CallContextFactory> emptyCallContextFactory = (Instance<CallContextFactory>) (Instance<?>) new ResolvedInstance<>(null);

assertDoesNotThrow(() -> new QuarkusGrpcHandler(
throwOnGet,
throwOnGet,
null,
emptyCallContextFactory,
Runnable::run));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package org.a2aproject.sdk.server;

import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.Iterator;

import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.UnsatisfiedResolutionException;
import jakarta.enterprise.util.TypeLiteral;

import org.jspecify.annotations.Nullable;

/**
* An {@link Instance} wrapper for a pre-resolved value, for use in non-CDI contexts
* such as convenience constructors and tests.
*
* @param <T> the bean type
*/
public class ResolvedInstance<T> implements Instance<T> {

private final @Nullable T value;

public ResolvedInstance(@Nullable T value) {
this.value = value;
}

@Override
public T get() {
if (value == null) {
throw new UnsatisfiedResolutionException("No value available");
}
return value;
}

@Override
public boolean isUnsatisfied() {
return value == null;
}

@Override
public boolean isAmbiguous() {
return false;
}

@Override
public Iterator<T> iterator() {
return value != null ? Collections.singleton(value).iterator() : Collections.emptyIterator();
}

@Override
public void destroy(T instance) {
}

@Override
public Instance<T> select(Annotation... qualifiers) {
throw new UnsupportedOperationException();
}

@Override
public <U extends T> Instance<U> select(Class<U> subtype, Annotation... qualifiers) {
throw new UnsupportedOperationException();
}

@Override
public <U extends T> Instance<U> select(TypeLiteral<U> subtype, Annotation... qualifiers) {
throw new UnsupportedOperationException();
}

@Override
public Handle<T> getHandle() {
throw new UnsupportedOperationException();
}

@Override
public Iterable<? extends Handle<T>> handles() {
throw new UnsupportedOperationException();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -833,8 +833,13 @@ private <V> void handleInternalError(StreamObserver<V> responseObserver, Throwab
private AgentCard getAgentCardInternal() {
AgentCard agentCard = getAgentCard();
if (initialised.compareAndSet(false, true)) {
// Validate transport configuration with proper classloader context
validateTransportConfigurationWithCorrectClassLoader(agentCard);
try {
// Validate transport configuration with proper classloader context
validateTransportConfigurationWithCorrectClassLoader(agentCard);
} catch (RuntimeException e) {
initialised.set(false);
throw e;
}
}
return agentCard;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicBoolean;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Instance;
Expand Down Expand Up @@ -35,6 +36,7 @@
import org.a2aproject.sdk.server.AgentCardValidator;
import org.a2aproject.sdk.server.ExtendedAgentCard;
import org.a2aproject.sdk.server.PublicAgentCard;
import org.a2aproject.sdk.server.ResolvedInstance;
import org.a2aproject.sdk.server.ServerCallContext;
import org.a2aproject.sdk.server.extensions.A2AExtensions;
import org.a2aproject.sdk.server.requesthandlers.RequestHandler;
Expand Down Expand Up @@ -135,10 +137,11 @@ public class JSONRPCHandler {
// Fields set by constructor injection cannot be final. We need a noargs constructor for
// Jakarta compatibility, and it seems that making fields set by constructor injection
// final, is not proxyable in all runtimes
private AgentCard agentCard;
private Instance<AgentCard> agentCardInstance;
private @Nullable Instance<AgentCard> extendedAgentCard;
private RequestHandler requestHandler;
private Executor executor;
private final AtomicBoolean transportValidated = new AtomicBoolean(false);

/**
* No-args constructor for CDI proxy creation.
Expand All @@ -148,7 +151,7 @@ public class JSONRPCHandler {
@SuppressWarnings("NullAway")
protected JSONRPCHandler() {
// For CDI proxy creation
this.agentCard = null;
this.agentCardInstance = null;
this.extendedAgentCard = null;
this.requestHandler = null;
this.executor = null;
Expand All @@ -157,21 +160,19 @@ protected JSONRPCHandler() {
/**
* Creates a JSON-RPC handler with full CDI injection support.
*
* @param agentCard the public agent card containing agent capabilities
* @param agentCardInstance the public agent card instance containing agent capabilities
* @param extendedAgentCard optional extended agent card instance
* @param requestHandler the handler for processing A2A requests
* @param executor the executor for asynchronous operations
*/
@Inject
public JSONRPCHandler(@PublicAgentCard AgentCard agentCard, @Nullable @ExtendedAgentCard Instance<AgentCard> extendedAgentCard,
public JSONRPCHandler(@PublicAgentCard Instance<AgentCard> agentCardInstance,
@Nullable @ExtendedAgentCard Instance<AgentCard> extendedAgentCard,
RequestHandler requestHandler, @Internal Executor executor) {
this.agentCard = agentCard;
this.agentCardInstance = agentCardInstance;
this.extendedAgentCard = extendedAgentCard;
this.requestHandler = requestHandler;
this.executor = executor;

// Validate transport configuration
AgentCardValidator.validateTransportConfiguration(agentCard);
}

/**
Expand All @@ -182,7 +183,7 @@ public JSONRPCHandler(@PublicAgentCard AgentCard agentCard, @Nullable @ExtendedA
* @param executor the executor for asynchronous operations
*/
public JSONRPCHandler(@PublicAgentCard AgentCard agentCard, RequestHandler requestHandler, Executor executor) {
this(agentCard, null, requestHandler, executor);
this(new ResolvedInstance<>(agentCard), null, requestHandler, executor);
}

/**
Expand Down Expand Up @@ -229,8 +230,8 @@ public JSONRPCHandler(@PublicAgentCard AgentCard agentCard, RequestHandler reque
*/
public SendMessageResponse onMessageSend(SendMessageRequest request, ServerCallContext context) {
try {
A2AVersionValidator.validateProtocolVersion(agentCard, context);
A2AExtensions.validateRequiredExtensions(agentCard, context);
A2AVersionValidator.validateProtocolVersion(agentCard(), context);
A2AExtensions.validateRequiredExtensions(agentCard(), context);
EventKind taskOrMessage = requestHandler.onMessageSend(request.getParams(), context);
return new SendMessageResponse(request.getId(), taskOrMessage);
} catch (A2AError e) {
Expand Down Expand Up @@ -278,16 +279,16 @@ public SendMessageResponse onMessageSend(SendMessageRequest request, ServerCallC
*/
public Flow.Publisher<SendStreamingMessageResponse> onMessageSendStream(
SendStreamingMessageRequest request, ServerCallContext context) {
if (!agentCard.capabilities().streaming()) {
if (!agentCard().capabilities().streaming()) {
return ZeroPublisher.fromItems(
new SendStreamingMessageResponse(
request.getId(),
new UnsupportedOperationError(null, "Streaming is not supported by the agent", null)));
}

try {
A2AVersionValidator.validateProtocolVersion(agentCard, context);
A2AExtensions.validateRequiredExtensions(agentCard, context);
A2AVersionValidator.validateProtocolVersion(agentCard(), context);
A2AExtensions.validateRequiredExtensions(agentCard(), context);
Flow.Publisher<StreamingEventKind> publisher =
requestHandler.onMessageSendStream(request.getParams(), context);
// We can't use the convertingProcessor convenience method since that propagates any errors as an error handled
Expand Down Expand Up @@ -375,7 +376,7 @@ public CancelTaskResponse onCancelTask(CancelTaskRequest request, ServerCallCont
*/
public Flow.Publisher<SendStreamingMessageResponse> onSubscribeToTask(
SubscribeToTaskRequest request, ServerCallContext context) throws A2AError {
if (!agentCard.capabilities().streaming()) {
if (!agentCard().capabilities().streaming()) {
return ZeroPublisher.fromItems(
new SendStreamingMessageResponse(
request.getId(),
Expand Down Expand Up @@ -422,7 +423,7 @@ public Flow.Publisher<SendStreamingMessageResponse> onSubscribeToTask(
*/
public GetTaskPushNotificationConfigResponse getPushNotificationConfig(
GetTaskPushNotificationConfigRequest request, ServerCallContext context) {
if (!agentCard.capabilities().pushNotifications()) {
if (!agentCard().capabilities().pushNotifications()) {
return new GetTaskPushNotificationConfigResponse(request.getId(),
new PushNotificationNotSupportedError());
}
Expand Down Expand Up @@ -464,7 +465,7 @@ public GetTaskPushNotificationConfigResponse getPushNotificationConfig(
*/
public CreateTaskPushNotificationConfigResponse setPushNotificationConfig(
CreateTaskPushNotificationConfigRequest request, ServerCallContext context) {
if (!agentCard.capabilities().pushNotifications()) {
if (!agentCard().capabilities().pushNotifications()) {
return new CreateTaskPushNotificationConfigResponse(request.getId(),
new PushNotificationNotSupportedError());
}
Expand Down Expand Up @@ -587,7 +588,7 @@ public ListTasksResponse onListTasks(ListTasksRequest request, ServerCallContext
*/
public ListTaskPushNotificationConfigsResponse listPushNotificationConfigs(
ListTaskPushNotificationConfigsRequest request, ServerCallContext context) {
if ( !agentCard.capabilities().pushNotifications()) {
if (!agentCard().capabilities().pushNotifications()) {
return new ListTaskPushNotificationConfigsResponse(request.getId(),
new PushNotificationNotSupportedError());
}
Expand Down Expand Up @@ -629,7 +630,7 @@ public ListTaskPushNotificationConfigsResponse listPushNotificationConfigs(
*/
public DeleteTaskPushNotificationConfigResponse deletePushNotificationConfig(
DeleteTaskPushNotificationConfigRequest request, ServerCallContext context) {
if ( !agentCard.capabilities().pushNotifications()) {
if (!agentCard().capabilities().pushNotifications()) {
return new DeleteTaskPushNotificationConfigResponse(request.getId(),
new PushNotificationNotSupportedError());
}
Expand Down Expand Up @@ -669,7 +670,7 @@ public DeleteTaskPushNotificationConfigResponse deletePushNotificationConfig(
// TODO: Add authentication (https://github.com/a2aproject/a2a-java/issues/77)
public GetExtendedAgentCardResponse onGetExtendedCardRequest(
GetExtendedAgentCardRequest request, ServerCallContext context) {
if (!agentCard.capabilities().extendedAgentCard()) {
if (!agentCard().capabilities().extendedAgentCard()) {
return new GetExtendedAgentCardResponse(request.getId(), new UnsupportedOperationError());
}
if (extendedAgentCard == null || !extendedAgentCard.isResolvable()) {
Expand All @@ -696,7 +697,20 @@ public GetExtendedAgentCardResponse onGetExtendedCardRequest(
* @see AgentCard
*/
public AgentCard getAgentCard() {
return agentCard;
return agentCard();
}

private AgentCard agentCard() {
AgentCard card = agentCardInstance.get();
if (transportValidated.compareAndSet(false, true)) {
try {
AgentCardValidator.validateTransportConfiguration(card);
} catch (RuntimeException e) {
transportValidated.set(false);
throw e;
}
}
return card;
}

private Flow.Publisher<SendStreamingMessageResponse> convertToSendStreamingMessageResponse(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.a2aproject.sdk.transport.jsonrpc.handler;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
Expand Down Expand Up @@ -80,6 +81,7 @@
import org.a2aproject.sdk.spec.TextPart;
import org.a2aproject.sdk.spec.UnsupportedOperationError;
import org.a2aproject.sdk.spec.VersionNotSupportedError;
import jakarta.enterprise.inject.Instance;
import mutiny.zero.ZeroPublisher;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Disabled;
Expand Down Expand Up @@ -2004,4 +2006,13 @@ public void testListTasksEmptyResultIncludesAllFields() {
assertEquals(0, result.pageSize(), "pageSize should be 0");
// nextPageToken can be null for empty results
}

@Test
void constructorDoesNotResolveAgentCardInstance() {
@SuppressWarnings("unchecked")
Instance<AgentCard> throwOnGet = Mockito.mock(Instance.class);
Mockito.when(throwOnGet.get()).thenThrow(new AssertionError("Instance.get() must not be called during construction"));

assertDoesNotThrow(() -> new JSONRPCHandler(throwOnGet, null, requestHandler, internalExecutor));
}
}
Loading
Loading