From 7d9bdcb56b64bbf79af24c673bfb468b0fd994f3 Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Mon, 10 Nov 2025 11:44:45 +0100 Subject: [PATCH 1/4] fix serialization in chat completions on tools --- .../ChatCompletions/Models/Tool.cs | 6 +- .../ChatCompletions/tools/request.json | 53 +++++++ .../ChatCompletions/tools/response.json | 42 ++++++ ....Agents.AI.Hosting.OpenAI.UnitTests.csproj | 12 -- .../OpenAIChatCompletionsConformanceTests.cs | 131 ++++++++++++++++++ 5 files changed, 229 insertions(+), 15 deletions(-) create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/ChatCompletions/tools/request.json create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/ChatCompletions/tools/response.json diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/Models/Tool.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/Models/Tool.cs index 470f7d15b0..87b0637b9b 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/Models/Tool.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/ChatCompletions/Models/Tool.cs @@ -18,7 +18,7 @@ internal abstract record Tool /// /// The type of the tool. /// - [JsonPropertyName("type")] + [JsonIgnore] public abstract string Type { get; } } @@ -30,7 +30,7 @@ internal sealed record FunctionTool : Tool /// /// The type of the tool. Always "function". /// - [JsonPropertyName("type")] + [JsonIgnore] public override string Type => "function"; /// @@ -88,7 +88,7 @@ internal sealed record CustomTool : Tool /// /// The type of the tool. Always "custom". /// - [JsonPropertyName("type")] + [JsonIgnore] public override string Type => "custom"; /// diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/ChatCompletions/tools/request.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/ChatCompletions/tools/request.json new file mode 100644 index 0000000000..b41ac7ab2e --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/ChatCompletions/tools/request.json @@ -0,0 +1,53 @@ +{ + "model": "gpt-4o-mini", + "messages": [ + { + "role": "user", + "content": "What's the weather like in San Francisco?" + } + ], + "max_completion_tokens": 256, + "temperature": 0.7, + "top_p": 1, + "tools": [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get the current weather in a given location", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + }, + "unit": { + "type": "string", + "enum": [ "celsius", "fahrenheit" ], + "description": "Temperature unit" + } + }, + "required": [ "location" ] + } + } + }, + { + "type": "function", + "function": { + "name": "get_time", + "description": "Get the current time in a given timezone", + "parameters": { + "type": "object", + "properties": { + "timezone": { + "type": "string", + "description": "The IANA timezone, e.g. America/Los_Angeles" + } + }, + "required": [ "timezone" ] + } + } + } + ] +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/ChatCompletions/tools/response.json b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/ChatCompletions/tools/response.json new file mode 100644 index 0000000000..b86280bca0 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/ConformanceTraces/ChatCompletions/tools/response.json @@ -0,0 +1,42 @@ +{ + "id": "chatcmpl-tools-test-001", + "object": "chat.completion", + "created": 1234567890, + "model": "gpt-4o-mini", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_abc123", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\"location\": \"San Francisco, CA\", \"unit\": \"fahrenheit\"}" + } + } + ] + }, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 85, + "completion_tokens": 32, + "total_tokens": 117, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default" +} \ No newline at end of file diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj index bd98aebed2..7d64f7ae2b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj @@ -27,16 +27,4 @@ - - - - - - - - - - - - diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIChatCompletionsConformanceTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIChatCompletionsConformanceTests.cs index b777db0ce5..ac6f2e234e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIChatCompletionsConformanceTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIChatCompletionsConformanceTests.cs @@ -456,6 +456,137 @@ public async Task JsonModeRequestResponseAsync() Assert.Equal(JsonValueKind.String, jsonRoot.GetProperty("occupation").ValueKind); } + [Fact] + public async Task ToolsSerializationDeserializationAsync() + { + // Arrange + string requestJson = LoadChatCompletionsTraceFile("tools/request.json"); + using var expectedResponseDoc = LoadChatCompletionsTraceDocument("tools/response.json"); + var expectedResponse = expectedResponseDoc.RootElement; + + HttpClient client = await this.CreateTestServerAsync( + "tools-agent", + "You are a helpful assistant with access to weather and time tools.", + "tool-call", + (msg) => [new FunctionCallContent("call_abc123", "get_weather", new Dictionary() { + { "location", "San Francisco, CA" }, + { "unit", "fahrenheit" } + })] + ); + + // Act + HttpResponseMessage httpResponse = await this.SendChatCompletionRequestAsync(client, "tools-agent", requestJson); + using var responseDoc = await ParseResponseAsync(httpResponse); + var response = responseDoc.RootElement; + + // Parse the request + using var requestDoc = JsonDocument.Parse(requestJson); + var request = requestDoc.RootElement; + + // Assert - Request has tools array with proper structure + AssertJsonPropertyExists(request, "tools"); + var tools = request.GetProperty("tools"); + Assert.Equal(JsonValueKind.Array, tools.ValueKind); + Assert.Equal(2, tools.GetArrayLength()); + + // Assert - First tool (get_weather) + var weatherTool = tools[0]; + AssertJsonPropertyEquals(weatherTool, "type", "function"); + AssertJsonPropertyExists(weatherTool, "function"); + + var weatherFunction = weatherTool.GetProperty("function"); + AssertJsonPropertyEquals(weatherFunction, "name", "get_weather"); + AssertJsonPropertyExists(weatherFunction, "description"); + AssertJsonPropertyExists(weatherFunction, "parameters"); + + var weatherParams = weatherFunction.GetProperty("parameters"); + AssertJsonPropertyEquals(weatherParams, "type", "object"); + AssertJsonPropertyExists(weatherParams, "properties"); + AssertJsonPropertyExists(weatherParams, "required"); + + // Verify location property exists + var properties = weatherParams.GetProperty("properties"); + AssertJsonPropertyExists(properties, "location"); + AssertJsonPropertyExists(properties, "unit"); + + // Assert - Second tool (get_time) + var timeTool = tools[1]; + AssertJsonPropertyEquals(timeTool, "type", "function"); + + var timeFunction = timeTool.GetProperty("function"); + AssertJsonPropertyEquals(timeFunction, "name", "get_time"); + AssertJsonPropertyExists(timeFunction, "description"); + AssertJsonPropertyExists(timeFunction, "parameters"); + + // Assert - Response structure + AssertJsonPropertyExists(response, "id"); + AssertJsonPropertyEquals(response, "object", "chat.completion"); + AssertJsonPropertyExists(response, "created"); + AssertJsonPropertyExists(response, "model"); + + // Assert - Response has tool_calls in choices + var choices = response.GetProperty("choices"); + Assert.Equal(JsonValueKind.Array, choices.ValueKind); + Assert.True(choices.GetArrayLength() > 0); + + var choice = choices[0]; + AssertJsonPropertyExists(choice, "finish_reason"); + AssertJsonPropertyEquals(choice, "finish_reason", anyOfValues: ["tool_calls", "stop"]); + AssertJsonPropertyExists(choice, "message"); + + var message = choice.GetProperty("message"); + AssertJsonPropertyEquals(message, "role", "assistant"); + AssertJsonPropertyExists(message, "tool_calls"); + + // Assert - Tool calls array structure + var toolCalls = message.GetProperty("tool_calls"); + Assert.Equal(JsonValueKind.Array, toolCalls.ValueKind); + Assert.True(toolCalls.GetArrayLength() > 0); + + var toolCall = toolCalls[0]; + AssertJsonPropertyExists(toolCall, "id"); + AssertJsonPropertyEquals(toolCall, "type", "function"); + AssertJsonPropertyExists(toolCall, "function"); + + var callFunction = toolCall.GetProperty("function"); + AssertJsonPropertyEquals(callFunction, "name", "get_weather"); + AssertJsonPropertyExists(callFunction, "arguments"); + + // Assert - Tool call arguments are valid JSON + string arguments = callFunction.GetProperty("arguments").GetString()!; + using var argsDoc = JsonDocument.Parse(arguments); + var argsRoot = argsDoc.RootElement; + AssertJsonPropertyExists(argsRoot, "location"); + AssertJsonPropertyEquals(argsRoot, "location", "San Francisco, CA"); + AssertJsonPropertyEquals(argsRoot, "unit", "fahrenheit"); + + // Assert - Message content is null when tool_calls present + if (message.TryGetProperty("content", out var contentProp)) + { + Assert.Equal(JsonValueKind.Null, contentProp.ValueKind); + } + + // Assert - Usage statistics + AssertJsonPropertyExists(response, "usage"); + var usage = response.GetProperty("usage"); + AssertJsonPropertyExists(usage, "prompt_tokens"); + AssertJsonPropertyExists(usage, "completion_tokens"); + AssertJsonPropertyExists(usage, "total_tokens"); + + var promptTokens = usage.GetProperty("prompt_tokens").GetInt32(); + var completionTokens = usage.GetProperty("completion_tokens").GetInt32(); + var totalTokens = usage.GetProperty("total_tokens").GetInt32(); + + Assert.True(promptTokens > 0); + Assert.True(completionTokens > 0); + Assert.Equal(promptTokens + completionTokens, totalTokens); + + // Assert - Service tier + AssertJsonPropertyExists(response, "service_tier"); + var serviceTier = response.GetProperty("service_tier").GetString(); + Assert.NotNull(serviceTier); + } + /// /// Helper to parse chat completion chunks from SSE response. /// From fa68b4d6ce14599b74c180044c792108a33ca4ec Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Mon, 10 Nov 2025 11:58:35 +0100 Subject: [PATCH 2/4] nit --- .../OpenAIChatCompletionsConformanceTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIChatCompletionsConformanceTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIChatCompletionsConformanceTests.cs index ac6f2e234e..8a38389035 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIChatCompletionsConformanceTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIChatCompletionsConformanceTests.cs @@ -462,7 +462,6 @@ public async Task ToolsSerializationDeserializationAsync() // Arrange string requestJson = LoadChatCompletionsTraceFile("tools/request.json"); using var expectedResponseDoc = LoadChatCompletionsTraceDocument("tools/response.json"); - var expectedResponse = expectedResponseDoc.RootElement; HttpClient client = await this.CreateTestServerAsync( "tools-agent", From 28818fff1b31af556d76aaa40885dc7db332a4a1 Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Mon, 10 Nov 2025 12:35:33 +0100 Subject: [PATCH 3/4] write e2e test for agent card resolve + adjust behavior --- .../AgentWebChat.AgentHost/Program.cs | 4 +- .../AIAgentExtensions.cs | 11 ++- .../A2AIntegrationTests.cs | 87 +++++++++++++++++++ .../EndpointRouteA2ABuilderExtensionsTests.cs | 25 +----- .../Internal/DummyChatClient.cs | 30 +++++++ ...oft.Agents.AI.Hosting.A2A.UnitTests.csproj | 5 +- 6 files changed, 134 insertions(+), 28 deletions(-) create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AIntegrationTests.cs create mode 100644 dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Internal/DummyChatClient.cs diff --git a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs index cb1a7e3cd9..46af2a5b19 100644 --- a/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs +++ b/dotnet/samples/AgentWebChat/AgentWebChat.AgentHost/Program.cs @@ -107,8 +107,8 @@ Once the user has deduced what type (knight or knave) both Alice and Bob are, te app.UseExceptionHandler(); // attach a2a with simple message communication -app.MapA2A(agentName: "pirate", path: "/a2a/pirate"); -app.MapA2A(agentName: "knights-and-knaves", path: "/a2a/knights-and-knaves", agentCard: new() +app.MapA2A(pirateAgentBuilder, path: "/a2a/pirate"); +app.MapA2A(knightsKnavesAgentBuilder, path: "/a2a/knights-and-knaves", agentCard: new() { Name = "Knights and Knaves", Description = "An agent that helps you solve the knights and knaves puzzle.", diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs index 43376d8fb2..3f6be3e89c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs @@ -83,7 +83,16 @@ public static ITaskManager MapA2A( { // A2A SDK assigns the url on its own // we can help user if they did not set Url explicitly. - agentCard.Url ??= context; + if (string.IsNullOrEmpty(agentCard.Url)) + { + var agentCardUrl = context; + if (!context.EndsWith("/v1/card", StringComparison.Ordinal)) + { + agentCardUrl += "/v1/card"; + } + + agentCard.Url = agentCardUrl; + } return Task.FromResult(agentCard); }; diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AIntegrationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AIntegrationTests.cs new file mode 100644 index 0000000000..48cb19789a --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/A2AIntegrationTests.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; +using System.Threading.Tasks; +using A2A; +using Microsoft.Agents.AI.Hosting.A2A.UnitTests.Internal; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests; + +public sealed class A2AIntegrationTests +{ + /// + /// Verifies that calling the A2A card endpoint with MapA2A returns an agent card with a URL populated. + /// + [Fact] + public async Task MapA2A_WithAgentCard_CardEndpointReturnsCardWithUrlAsync() + { + // Arrange + WebApplicationBuilder builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + + IChatClient mockChatClient = new DummyChatClient(); + builder.Services.AddKeyedSingleton("chat-client", mockChatClient); + IHostedAgentBuilder agentBuilder = builder.AddAIAgent("test-agent", "Test instructions", chatClientServiceKey: "chat-client"); + builder.Services.AddLogging(); + + using WebApplication app = builder.Build(); + + var agentCard = new AgentCard + { + Name = "Test Agent", + Description = "A test agent for A2A communication", + Version = "1.0" + }; + + // Map A2A with the agent card + app.MapA2A(agentBuilder, "/a2a/test-agent", agentCard); + + await app.StartAsync(); + + try + { + // Get the test server client + TestServer testServer = app.Services.GetRequiredService() as TestServer + ?? throw new InvalidOperationException("TestServer not found"); + var httpClient = testServer.CreateClient(); + + // Act - Query the agent card endpoint + var requestUri = new Uri("/a2a/test-agent/v1/card", UriKind.Relative); + var response = await httpClient.GetAsync(requestUri); + + // Assert + Assert.True(response.IsSuccessStatusCode, $"Expected successful response but got {response.StatusCode}"); + + var content = await response.Content.ReadAsStringAsync(); + var jsonDoc = JsonDocument.Parse(content); + var root = jsonDoc.RootElement; + + // Verify the card has expected properties + Assert.True(root.TryGetProperty("name", out var nameProperty)); + Assert.Equal("Test Agent", nameProperty.GetString()); + + Assert.True(root.TryGetProperty("description", out var descProperty)); + Assert.Equal("A test agent for A2A communication", descProperty.GetString()); + + // Verify the card has a URL property and it's not null/empty + Assert.True(root.TryGetProperty("url", out var urlProperty)); + Assert.NotEqual(JsonValueKind.Null, urlProperty.ValueKind); + + var url = urlProperty.GetString(); + Assert.NotNull(url); + Assert.NotEmpty(url); + Assert.StartsWith("http", url, StringComparison.OrdinalIgnoreCase); + Assert.Equal($"{testServer.BaseAddress.ToString().TrimEnd('/')}/a2a/test-agent/v1/card", url); + } + finally + { + await app.StopAsync(); + } + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/EndpointRouteA2ABuilderExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/EndpointRouteA2ABuilderExtensionsTests.cs index 1ae0dda908..a848528888 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/EndpointRouteA2ABuilderExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/EndpointRouteA2ABuilderExtensionsTests.cs @@ -1,10 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using A2A; +using Microsoft.Agents.AI.Hosting.A2A.UnitTests.Internal; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; @@ -478,25 +476,4 @@ public void MapA2A_WithAgentBuilder_FullAgentCard_Succeeds() var result = app.MapA2A(agentBuilder, "/a2a", agentCard); Assert.NotNull(result); } - - private sealed class DummyChatClient : IChatClient - { - public void Dispose() - { - throw new NotImplementedException(); - } - - public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - public object? GetService(Type serviceType, object? serviceKey = null) => - serviceType.IsInstanceOfType(this) ? this : null; - - public IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Internal/DummyChatClient.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Internal/DummyChatClient.cs new file mode 100644 index 0000000000..efab140b68 --- /dev/null +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Internal/DummyChatClient.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.AI; + +namespace Microsoft.Agents.AI.Hosting.A2A.UnitTests.Internal; + +internal sealed class DummyChatClient : IChatClient +{ + public void Dispose() + { + throw new NotImplementedException(); + } + + public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public object? GetService(Type serviceType, object? serviceKey = null) => + serviceType.IsInstanceOfType(this) ? this : null; + + public IAsyncEnumerable GetStreamingResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } +} diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj index 63387ae458..07dde4f802 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj @@ -1,4 +1,4 @@ - + $(ProjectsCoreTargetFrameworks) @@ -6,6 +6,9 @@ + + + From 14ec29afc231662d40c051ed9d95e0656d4b15e1 Mon Sep 17 00:00:00 2001 From: Korolev Dmitry Date: Mon, 10 Nov 2025 12:57:53 +0100 Subject: [PATCH 4/4] nit --- dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs index 3f6be3e89c..c54af66bb8 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.A2A/AIAgentExtensions.cs @@ -85,7 +85,7 @@ public static ITaskManager MapA2A( // we can help user if they did not set Url explicitly. if (string.IsNullOrEmpty(agentCard.Url)) { - var agentCardUrl = context; + var agentCardUrl = context.TrimEnd('/'); if (!context.EndsWith("/v1/card", StringComparison.Ordinal)) { agentCardUrl += "/v1/card";