Skip to content
Merged
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 @@ -2,6 +2,7 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
Expand Down Expand Up @@ -96,6 +97,38 @@ public void testGetAgentCardJsonDecodeError() throws Exception {
}


@Test
public void testParseV1AgentCardWithUrlAndPreferredTransport() throws Exception {
TestHttpClient client = new TestHttpClient();
client.body = JsonMessages_v0_3.V1_AGENT_CARD;

A2ACardResolver_v0_3 resolver = new A2ACardResolver_v0_3(client, "http://example.com/");
AgentCard_v0_3 card = resolver.getAgentCard();

assertEquals("Test Agent", card.name());
assertEquals("A test agent", card.description());
assertEquals("1.0.0", card.version());
assertEquals("https://agent.example.com/a2a", card.url());
assertEquals("JSONRPC", card.preferredTransport());
assertTrue(card.capabilities().streaming());
}

@Test
public void testParseV1AgentCardDefaultsPreferredTransportWhenAbsent() throws Exception {
TestHttpClient client = new TestHttpClient();
client.body = JsonMessages_v0_3.V1_AGENT_CARD_NO_PREFERRED_TRANSPORT;

A2ACardResolver_v0_3 resolver = new A2ACardResolver_v0_3(client, "http://example.com/");
AgentCard_v0_3 card = resolver.getAgentCard();

assertEquals("Minimal Agent", card.name());
assertEquals("https://agent.example.com/a2a", card.url());
// v0.3 compact constructor defaults null preferredTransport to "JSONRPC"
assertEquals("JSONRPC", card.preferredTransport());
// v1.0-only fields such as supportedInterfaces are unknown to v0.3 and must be ignored
assertNull(card.additionalInterfaces());
}

@Test
public void testGetAgentCardRequestError() throws Exception {
TestHttpClient client = new TestHttpClient();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,56 @@ public class JsonMessages_v0_3 {
]
}""";

/**
* A v1.0 format agent card returned by a current server. Contains the new {@code supportedInterfaces}
* and {@code securityRequirements} fields (v1.0-only), plus the backward-compat {@code url} and
* {@code preferredTransport} fields that the v0.3 client relies on.
*/
static final String V1_AGENT_CARD = """
{
"name": "Test Agent",
"description": "A test agent",
"url": "https://agent.example.com/a2a",
"preferredTransport": "JSONRPC",
"version": "1.0.0",
"supportedInterfaces": [
{"protocolBinding": "JSONRPC", "url": "https://agent.example.com/a2a"},
{"protocolBinding": "GRPC", "url": "grpc://agent.example.com:9090"}
],
"capabilities": {
"streaming": true,
"pushNotifications": false,
"stateTransitionHistory": false
},
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["text/plain"],
"skills": [],
"securityRequirements": [{"oauth2": ["read"]}]
}""";

/**
* A v1.0 format agent card without an explicit {@code preferredTransport}. The v0.3 client must
* fall back to the default transport ("JSONRPC").
*/
static final String V1_AGENT_CARD_NO_PREFERRED_TRANSPORT = """
{
"name": "Minimal Agent",
"description": "Minimal agent card",
"url": "https://agent.example.com/a2a",
"version": "2.0.0",
"supportedInterfaces": [
{"protocolBinding": "JSONRPC", "url": "https://agent.example.com/a2a"}
],
"capabilities": {
"streaming": false,
"pushNotifications": false,
"stateTransitionHistory": false
},
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["text/plain"],
"skills": []
}""";

static final String AUTHENTICATION_EXTENDED_AGENT_CARD = """
{
"name": "GeoSpatial Route Planner Agent Extended",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ public AgentCard getAgentCard() throws A2AClientError, A2AClientJSONError {

try {
org.a2aproject.sdk.grpc.AgentCard.Builder agentCardBuilder = org.a2aproject.sdk.grpc.AgentCard.newBuilder();
JSONRPCUtils.parseJsonString(body, agentCardBuilder, "");
JSONRPCUtils.parseJsonString(body, agentCardBuilder, "", true);
return ProtoUtils.FromProto.agentCard(agentCardBuilder);
} catch (A2AError | JsonProcessingException e) {
throw new A2AClientJSONError("Could not unmarshal agent card response", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,7 @@ public interface AgentCardMapper {
@Mapping(target = "provider", source = "provider", conditionExpression = "java(proto.hasProvider())")
@Mapping(target = "documentationUrl", source = "documentationUrl", conditionExpression = "java(!proto.getDocumentationUrl().isEmpty())")
@Mapping(target = "iconUrl", source = "iconUrl", conditionExpression = "java(!proto.getIconUrl().isEmpty())")
@Mapping(target = "url", ignore = true)
@Mapping(target = "preferredTransport", ignore = true)
Comment thread
ehsavoie marked this conversation as resolved.
org.a2aproject.sdk.spec.AgentCard fromProto(org.a2aproject.sdk.grpc.AgentCard proto);
}
Original file line number Diff line number Diff line change
Expand Up @@ -439,8 +439,13 @@ protected static void parseRequestBody(JsonElement jsonRpc, com.google.protobuf.
}

public static void parseJsonString(String body, com.google.protobuf.Message.Builder builder, Object id) throws JsonProcessingException {
parseJsonString(body, builder, id, false);
}

public static void parseJsonString(String body, com.google.protobuf.Message.Builder builder, Object id, boolean ignoringUnknownFields) throws JsonProcessingException {
try {
JsonFormat.parser().merge(body, builder);
JsonFormat.Parser parser = ignoringUnknownFields ? JsonFormat.parser().ignoringUnknownFields() : JsonFormat.parser();
parser.merge(body, builder);
} catch (InvalidProtocolBufferException e) {
log.log(Level.FINE, "Protocol buffer parsing failed for JSON: {0}", body);
log.log(Level.FINE, "Proto parsing error details", e);
Expand Down
50 changes: 34 additions & 16 deletions spec/src/main/java/org/a2aproject/sdk/spec/AgentCard.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ public record AgentCard(
@Nullable List<SecurityRequirement> securityRequirements,
@Nullable String iconUrl,
List<AgentInterface> supportedInterfaces,
@Nullable List<AgentCardSignature> signatures) {
@Nullable List<AgentCardSignature> signatures,
@Nullable String url,
@Nullable String preferredTransport) {

/**
* Compact constructor that validates required fields.
Expand Down Expand Up @@ -156,6 +158,8 @@ public static class Builder {
private @Nullable String iconUrl;
private @Nullable List<AgentInterface> supportedInterfaces;
private @Nullable List<AgentCardSignature> signatures;
private @Nullable String url;
private @Nullable String preferredTransport;

/**
* Creates a new Builder with all fields unset.
Expand All @@ -172,20 +176,22 @@ private Builder() {
* @param card the AgentCard to copy values from
*/
private Builder(AgentCard card) {
this.name = card.name;
this.description = card.description;
this.provider = card.provider;
this.version = card.version;
this.documentationUrl = card.documentationUrl;
this.capabilities = card.capabilities;
this.defaultInputModes = card.defaultInputModes != null ? new ArrayList<>(card.defaultInputModes) : Collections.emptyList();
this.defaultOutputModes = card.defaultOutputModes != null ? new ArrayList<>(card.defaultOutputModes) : Collections.emptyList();
this.skills = card.skills != null ? new ArrayList<>(card.skills) : Collections.emptyList();
this.securitySchemes = card.securitySchemes != null ? Map.copyOf(card.securitySchemes) : Collections.emptyMap();
this.securityRequirements = card.securityRequirements != null ? new ArrayList<>(card.securityRequirements) : Collections.emptyList();
this.iconUrl = card.iconUrl;
this.supportedInterfaces = card.supportedInterfaces != null ? new ArrayList<>(card.supportedInterfaces) : Collections.emptyList();
this.signatures = card.signatures != null ? new ArrayList<>(card.signatures) : null;
this.name = card.name();
this.description = card.description();
this.provider = card.provider();
this.version = card.version();
this.documentationUrl = card.documentationUrl();
this.capabilities = card.capabilities();
this.defaultInputModes = card.defaultInputModes() != null ? new ArrayList<>(card.defaultInputModes()) : Collections.emptyList();
this.defaultOutputModes = card.defaultOutputModes() != null ? new ArrayList<>(card.defaultOutputModes()) : Collections.emptyList();
this.skills = card.skills() != null ? new ArrayList<>(card.skills()) : Collections.emptyList();
this.securitySchemes = card.securitySchemes() != null ? Map.copyOf(card.securitySchemes()) : Collections.emptyMap();
this.securityRequirements = card.securityRequirements() != null ? new ArrayList<>(card.securityRequirements()) : Collections.emptyList();
this.iconUrl = card.iconUrl();
this.supportedInterfaces = card.supportedInterfaces() != null ? new ArrayList<>(card.supportedInterfaces()) : Collections.emptyList();
this.signatures = card.signatures() != null ? new ArrayList<>(card.signatures()) : null;
this.url = card.url();
this.preferredTransport= card.preferredTransport();
}

/**
Expand Down Expand Up @@ -376,6 +382,16 @@ public Builder signatures(List<AgentCardSignature> signatures) {
return this;
}

public Builder preferredTransport(String preferredTransport) {
this.preferredTransport = preferredTransport;
return this;
}

public Builder url(String url) {
this.url = url;
return this;
}

/**
* Builds an immutable {@link AgentCard} from the current builder state.
* <p>
Expand All @@ -399,7 +415,9 @@ public AgentCard build() {
securityRequirements,
iconUrl,
Assert.checkNotNullParam("supportedInterfaces", supportedInterfaces),
signatures);
signatures,
url,
preferredTransport == null ? "JSONRPC" : preferredTransport);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ public AgentCard agentCard() {
.defaultInputModes(Collections.singletonList("text"))
.defaultOutputModes(Collections.singletonList("text"))
.skills(new ArrayList<>())
.supportedInterfaces(Collections.singletonList(new AgentInterface(preferredTransport, transportUrl)));
.supportedInterfaces(Collections.singletonList(new AgentInterface(preferredTransport, transportUrl)))
.url(transportUrl)
.preferredTransport(preferredTransport);

// Add security configuration if enabled (for authentication tests)
if (securityEnabled) {
Expand Down