From 25c8984dd2db0f36d9dd6af38fe27d6a7a33849c Mon Sep 17 00:00:00 2001 From: "Darrel Miller (from Dev Box)" Date: Sun, 7 Jun 2026 18:36:21 -0400 Subject: [PATCH 1/2] feat: support v0.3 agent card parsing in A2ACardResolver When fetching an agent card, if deserialization fails due to missing v1.0 required properties (supportedInterfaces, skills, etc.), attempt to parse it as a v0.3 card and upcast to a valid v1.0 AgentCard in memory. v0.3 cards have a top-level 'url' and optional 'preferredTransport' instead of 'supportedInterfaces'. The upcast maps these to a single AgentInterface entry with the appropriate protocol binding (defaulting to JSONRPC). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/A2A/Client/A2ACardResolver.cs | 149 +++++++++++++++++++++++++++++- src/A2A/Log.cs | 3 + 2 files changed, 149 insertions(+), 3 deletions(-) diff --git a/src/A2A/Client/A2ACardResolver.cs b/src/A2A/Client/A2ACardResolver.cs index d098f879..87e57d35 100644 --- a/src/A2A/Client/A2ACardResolver.cs +++ b/src/A2A/Client/A2ACardResolver.cs @@ -68,10 +68,24 @@ public async Task 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) when (ex.Message.Contains("supportedInterfaces") || + ex.Message.Contains("skills") || + ex.Message.Contains("defaultInputModes") || + ex.Message.Contains("defaultOutputModes")) + { + // The card is missing v1.0 required properties — attempt v0.3 upcast + _logger.AttemptingV03AgentCardUpcast(ex); + return UpcastV03AgentCard(bytes) + ?? throw new A2AException($"Failed to parse JSON: {ex.Message}"); + } } catch (JsonException ex) { @@ -88,4 +102,133 @@ public async Task GetAgentCardAsync(CancellationToken cancellationTok throw new A2AException("HTTP request failed", ex); } } + + /// + /// Attempts to parse a v0.3 agent card and upcast it to a v1.0 . + /// A v0.3 card has a top-level "url" and optional "preferredTransport" instead of "supportedInterfaces". + /// + /// The raw JSON bytes of the agent card response. + /// An upcast v1.0 if the JSON is a valid v0.3 card; otherwise null. + private static AgentCard? UpcastV03AgentCard(byte[] bytes) + { + using var doc = JsonDocument.Parse(bytes); + var root = doc.RootElement; + + // 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; + } + + // 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.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 + { + new() + { + ProtocolBinding = protocolBinding, + Url = url, + ProtocolVersion = root.TryGetProperty("protocolVersion", out var pv) ? pv.GetString() ?? "0.3" : "0.3", + } + }; + + // Also include additionalInterfaces if present (a v0.3 extension) + if (root.TryGetProperty("additionalInterfaces", out var addlInterfaces) && addlInterfaces.ValueKind == JsonValueKind.Array) + { + 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.GetString() ?? "" : "", + Description = root.TryGetProperty("description", out var desc) ? desc.GetString() ?? "" : "", + Version = root.TryGetProperty("version", out var ver) ? 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)) + card.DocumentationUrl = docUrl.GetString(); + if (root.TryGetProperty("iconUrl", out var iconUrl)) + card.IconUrl = iconUrl.GetString(); + + return card; + } } diff --git a/src/A2A/Log.cs b/src/A2A/Log.cs index e8095dd9..af2d4a8c 100644 --- a/src/A2A/Log.cs +++ b/src/A2A/Log.cs @@ -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, "Agent card missing v1.0 required properties, 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); From fa5eb68d55688d9694039fdffe692766b7a5d40c Mon Sep 17 00:00:00 2001 From: "Darrel Miller (from Dev Box)" Date: Mon, 10 Aug 2026 11:40:10 -0400 Subject: [PATCH 2/2] fix: harden v0.3 agent card upcast fallback - Remove fragile exception message filtering; catch all JsonException - Add root object guard to prevent InvalidOperationException on non-object JSON - Require top-level protocolVersion as v0.3 discriminator - Add ValueKind checks on all GetString() calls - Update log message to reflect broader catch semantics Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/A2A/Client/A2ACardResolver.cs | 32 +++++++++++++++++++------------ src/A2A/Log.cs | 2 +- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/A2A/Client/A2ACardResolver.cs b/src/A2A/Client/A2ACardResolver.cs index 87e57d35..6652b85d 100644 --- a/src/A2A/Client/A2ACardResolver.cs +++ b/src/A2A/Client/A2ACardResolver.cs @@ -76,12 +76,9 @@ public async Task GetAgentCardAsync(CancellationToken cancellationTok return JsonSerializer.Deserialize(bytes, A2AJsonUtilities.JsonContext.Default.AgentCard) ?? throw new A2AException("Failed to parse agent card JSON."); } - catch (JsonException ex) when (ex.Message.Contains("supportedInterfaces") || - ex.Message.Contains("skills") || - ex.Message.Contains("defaultInputModes") || - ex.Message.Contains("defaultOutputModes")) + catch (JsonException ex) { - // The card is missing v1.0 required properties — attempt v0.3 upcast + // v1.0 deserialization failed — attempt v0.3 upcast _logger.AttemptingV03AgentCardUpcast(ex); return UpcastV03AgentCard(bytes) ?? throw new A2AException($"Failed to parse JSON: {ex.Message}"); @@ -114,6 +111,11 @@ public async Task GetAgentCardAsync(CancellationToken cancellationTok 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) { @@ -126,13 +128,19 @@ public async Task GetAgentCardAsync(CancellationToken cancellationTok 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) + : transportElement.ValueKind == JsonValueKind.Object && transportElement.TryGetProperty("value", out var val) && val.ValueKind == JsonValueKind.String ? val.GetString() : null; @@ -155,7 +163,7 @@ public async Task GetAgentCardAsync(CancellationToken cancellationTok { ProtocolBinding = protocolBinding, Url = url, - ProtocolVersion = root.TryGetProperty("protocolVersion", out var pv) ? pv.GetString() ?? "0.3" : "0.3", + ProtocolVersion = pvElement.GetString() ?? "0.3", } }; @@ -175,9 +183,9 @@ public async Task GetAgentCardAsync(CancellationToken cancellationTok // Extract common fields var card = new AgentCard { - Name = root.TryGetProperty("name", out var name) ? name.GetString() ?? "" : "", - Description = root.TryGetProperty("description", out var desc) ? desc.GetString() ?? "" : "", - Version = root.TryGetProperty("version", out var ver) ? ver.GetString() ?? "0.3" : "0.3", + Name = root.TryGetProperty("name", out var name) && name.ValueKind == JsonValueKind.String ? name.GetString() ?? "" : "", + 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"], @@ -224,9 +232,9 @@ public async Task GetAgentCardAsync(CancellationToken cancellationTok } } - if (root.TryGetProperty("documentationUrl", out var docUrl)) + if (root.TryGetProperty("documentationUrl", out var docUrl) && docUrl.ValueKind == JsonValueKind.String) card.DocumentationUrl = docUrl.GetString(); - if (root.TryGetProperty("iconUrl", out var iconUrl)) + if (root.TryGetProperty("iconUrl", out var iconUrl) && iconUrl.ValueKind == JsonValueKind.String) card.IconUrl = iconUrl.GetString(); return card; diff --git a/src/A2A/Log.cs b/src/A2A/Log.cs index af2d4a8c..c4538129 100644 --- a/src/A2A/Log.cs +++ b/src/A2A/Log.cs @@ -17,7 +17,7 @@ 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, "Agent card missing v1.0 required properties, attempting v0.3 upcast")] + [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}")]