-
Notifications
You must be signed in to change notification settings - Fork 66
feat: support v0.3 agent card parsing in A2ACardResolver #417
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
| { | ||
|
|
@@ -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) | ||
| { | ||
| 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() ?? "" : "", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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.