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
157 changes: 154 additions & 3 deletions src/A2A/Client/A2ACardResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,21 @@ public async Task<AgentCard> GetAgentCardAsync(CancellationToken cancellationTok

response.EnsureSuccessStatusCode();

using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
// Buffer the response so we can attempt multiple deserialization strategies
var bytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);

return await JsonSerializer.DeserializeAsync(responseStream, A2AJsonUtilities.JsonContext.Default.AgentCard, cancellationToken).ConfigureAwait(false) ??
throw new A2AException("Failed to parse agent card JSON.");
try
{
return JsonSerializer.Deserialize(bytes, A2AJsonUtilities.JsonContext.Default.AgentCard)
?? throw new A2AException("Failed to parse agent card JSON.");
}
catch (JsonException ex)
{
// v1.0 deserialization failed — attempt v0.3 upcast
_logger.AttemptingV03AgentCardUpcast(ex);
return UpcastV03AgentCard(bytes)
?? throw new A2AException($"Failed to parse JSON: {ex.Message}");
}
}
catch (JsonException ex)
{
Expand All @@ -88,4 +99,144 @@ public async Task<AgentCard> GetAgentCardAsync(CancellationToken cancellationTok
throw new A2AException("HTTP request failed", ex);
}
}

/// <summary>
/// Attempts to parse a v0.3 agent card and upcast it to a v1.0 <see cref="AgentCard"/>.
/// A v0.3 card has a top-level "url" and optional "preferredTransport" instead of "supportedInterfaces".
/// </summary>
/// <param name="bytes">The raw JSON bytes of the agent card response.</param>
/// <returns>An upcast v1.0 <see cref="AgentCard"/> if the JSON is a valid v0.3 card; otherwise <c>null</c>.</returns>
private static AgentCard? UpcastV03AgentCard(byte[] bytes)
{
using var doc = JsonDocument.Parse(bytes);
var root = doc.RootElement;

if (root.ValueKind != JsonValueKind.Object)
{
return null;
}

// v0.3 cards MUST have a "url" property
if (!root.TryGetProperty("url", out var urlElement) || urlElement.ValueKind != JsonValueKind.String)
{
return null;
}

var url = urlElement.GetString();
if (string.IsNullOrEmpty(url))
{
return null;
}

// v0.3 cards MUST have a top-level "protocolVersion" — use as a discriminator since we only reach here after v1.0 deserialization failed
if (!root.TryGetProperty("protocolVersion", out var pvElement) || pvElement.ValueKind != JsonValueKind.String)
{
return null;
}

// Determine the protocol binding from preferredTransport (defaults to JSONRPC)
var protocolBinding = ProtocolBindingNames.JsonRpc;
if (root.TryGetProperty("preferredTransport", out var transportElement))
{
var transport = transportElement.ValueKind == JsonValueKind.String
? transportElement.GetString()
: transportElement.ValueKind == JsonValueKind.Object && transportElement.TryGetProperty("value", out var val) && val.ValueKind == JsonValueKind.String
? val.GetString()
: null;

if (!string.IsNullOrEmpty(transport))
{
protocolBinding = transport.ToUpperInvariant() switch
{
"JSONRPC" or "JSON-RPC" => ProtocolBindingNames.JsonRpc,
"HTTP+JSON" or "HTTP_JSON" or "REST" => ProtocolBindingNames.HttpJson,
"GRPC" => ProtocolBindingNames.Grpc,
_ => transport
};
}
}

// Build the supportedInterfaces list from the v0.3 url + preferredTransport
var interfaces = new List<AgentInterface>
{
new()
{
ProtocolBinding = protocolBinding,
Url = url,
ProtocolVersion = pvElement.GetString() ?? "0.3",
}
};

// Also include additionalInterfaces if present (a v0.3 extension)
if (root.TryGetProperty("additionalInterfaces", out var addlInterfaces) && addlInterfaces.ValueKind == JsonValueKind.Array)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice direction on the v0.3 fallback. One spec-shape issue here: v0.3 additionalInterfaces entries are { "transport": "...", "url": "..." }, while the v1 AgentInterface type expects protocolBinding and protocolVersion.

So a valid v0.3 card with additional HTTP+JSON/GRPC interfaces can fail this fallback, or lose the transport binding. Could we parse this as the v0.3 shape explicitly and map:

transport → ProtocolBinding
url → Url
top-level protocolVersion → ProtocolVersion
A focused test with additionalInterfaces: [{ "transport": "HTTP+JSON", "url": "..." }] would catch this.

{
foreach (var iface in addlInterfaces.EnumerateArray())
{
var ai = JsonSerializer.Deserialize(iface.GetRawText(), A2AJsonUtilities.JsonContext.Default.AgentInterface);
if (ai is not null)
{
interfaces.Add(ai);
}
}
}

// Extract common fields
var card = new AgentCard
{
Name = root.TryGetProperty("name", out var name) && name.ValueKind == JsonValueKind.String ? name.GetString() ?? "" : "",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

several v0.3 AgentCard fields have v1 destinations but are not preserved here, especially provider, securitySchemes, security, supportsAuthenticatedExtendedCard, signatures, and skill-level security.

If the goal is only “resolve a usable endpoint from a v0.3 card,” this is fine but should be called out as scope. If the goal is “support v0.3 agent card parsing,” I think we should preserve those fields, especially the security metadata.

Description = root.TryGetProperty("description", out var desc) && desc.ValueKind == JsonValueKind.String ? desc.GetString() ?? "" : "",
Version = root.TryGetProperty("version", out var ver) && ver.ValueKind == JsonValueKind.String ? ver.GetString() ?? "0.3" : "0.3",
SupportedInterfaces = interfaces,
Capabilities = new AgentCapabilities(),
DefaultInputModes = ["text/plain"],
DefaultOutputModes = ["text/plain"],
Skills = [],
};

// Parse capabilities
if (root.TryGetProperty("capabilities", out var caps) && caps.ValueKind == JsonValueKind.Object)
{
if (caps.TryGetProperty("streaming", out var streaming))
card.Capabilities.Streaming = streaming.ValueKind == JsonValueKind.True;
if (caps.TryGetProperty("pushNotifications", out var push))
card.Capabilities.PushNotifications = push.ValueKind == JsonValueKind.True;
}

// Parse default modes if present
if (root.TryGetProperty("defaultInputModes", out var inputModes) && inputModes.ValueKind == JsonValueKind.Array)
{
card.DefaultInputModes = inputModes.EnumerateArray()
.Where(e => e.ValueKind == JsonValueKind.String)
.Select(e => e.GetString()!)
.ToList();
}

if (root.TryGetProperty("defaultOutputModes", out var outputModes) && outputModes.ValueKind == JsonValueKind.Array)
{
card.DefaultOutputModes = outputModes.EnumerateArray()
.Where(e => e.ValueKind == JsonValueKind.String)
.Select(e => e.GetString()!)
.ToList();
}

// Parse skills if present
if (root.TryGetProperty("skills", out var skills) && skills.ValueKind == JsonValueKind.Array)
{
foreach (var skillElement in skills.EnumerateArray())
{
var skill = JsonSerializer.Deserialize(skillElement.GetRawText(), A2AJsonUtilities.JsonContext.Default.AgentSkill);
if (skill is not null)
{
card.Skills.Add(skill);
}
}
}

if (root.TryGetProperty("documentationUrl", out var docUrl) && docUrl.ValueKind == JsonValueKind.String)
card.DocumentationUrl = docUrl.GetString();
if (root.TryGetProperty("iconUrl", out var iconUrl) && iconUrl.ValueKind == JsonValueKind.String)
card.IconUrl = iconUrl.GetString();

return card;
}
}
3 changes: 3 additions & 0 deletions src/A2A/Log.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ static partial class Log
[LoggerMessage(2, LogLevel.Error, "HTTP request failed with status code {StatusCode}")]
internal static partial void HttpRequestFailedWithStatusCode(this ILogger logger, Exception exception, System.Net.HttpStatusCode StatusCode);

[LoggerMessage(5, LogLevel.Information, "V1.0 agent card deserialization failed, attempting v0.3 upcast")]
internal static partial void AttemptingV03AgentCardUpcast(this ILogger logger, Exception exception);

[LoggerMessage(3, LogLevel.Error, "Background event processing failed for task {TaskId}")]
internal static partial void BackgroundEventProcessingFailed(this ILogger logger, Exception exception, string TaskId);

Expand Down