diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index b2dd14e9b0..83ac69796b 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -416,6 +416,10 @@ + + + + diff --git a/dotnet/eng/MSBuild/Shared.props b/dotnet/eng/MSBuild/Shared.props index 9b4771a64e..94ac5b417b 100644 --- a/dotnet/eng/MSBuild/Shared.props +++ b/dotnet/eng/MSBuild/Shared.props @@ -8,6 +8,9 @@ + + + diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index dcfcac4077..ee3b144b06 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -2,11 +2,11 @@ 1.0.0 - 2 + 3 $(VersionPrefix)-rc$(RCNumber) - $(VersionPrefix)-$(VersionSuffix).260225.1 - $(VersionPrefix)-preview.260225.1 - 1.0.0-rc2 + $(VersionPrefix)-$(VersionSuffix).260304.1 + $(VersionPrefix)-preview.260304.1 + 1.0.0-rc3 Debug;Release;Publish true diff --git a/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs index c966f591fc..bbebd7a312 100644 --- a/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs @@ -346,14 +346,14 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable }; } - private AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent assistantMessage) + internal AgentResponseUpdate ConvertToAgentResponseUpdate(AssistantMessageEvent assistantMessage) { - TextContent textContent = new(assistantMessage.Data?.Content ?? string.Empty) + AIContent content = new() { RawRepresentation = assistantMessage }; - return new AgentResponseUpdate(ChatRole.Assistant, [textContent]) + return new AgentResponseUpdate(ChatRole.Assistant, [content]) { AgentId = this.Id, ResponseId = assistantMessage.Data?.MessageId, diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs index 7db4eff6d8..e4b772160e 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgent.cs @@ -747,9 +747,15 @@ public sealed partial class ChatClientAgent : AIAgent { // The agent has a ChatHistoryProvider configured, but the service returned a conversation id, // meaning the service manages chat history server-side. Both cannot be used simultaneously. - if (this._agentOptions?.WarnOnChatHistoryProviderConflict is true) + if (this._agentOptions?.WarnOnChatHistoryProviderConflict is true + && this._logger.IsEnabled(LogLevel.Warning)) { - this._logger.LogAgentChatClientHistoryProviderConflict(nameof(ChatClientAgentSession.ConversationId), nameof(this.ChatHistoryProvider), this.Id, this.GetLoggingAgentName()); + var loggingAgentName = this.GetLoggingAgentName(); + this._logger.LogAgentChatClientHistoryProviderConflict( + nameof(ChatClientAgentSession.ConversationId), + nameof(this.ChatHistoryProvider), + this.Id, + loggingAgentName); } if (this._agentOptions?.ThrowOnChatHistoryProviderConflict is true) diff --git a/dotnet/src/Shared/IntegrationTestsAzureCredentials/README.md b/dotnet/src/Shared/IntegrationTestsAzureCredentials/README.md new file mode 100644 index 0000000000..e26295ed7f --- /dev/null +++ b/dotnet/src/Shared/IntegrationTestsAzureCredentials/README.md @@ -0,0 +1,9 @@ +# Integration Tests Azure Credentials + +Adds a helper for loading Azure credentials in integration tests. + +```xml + + true + +``` diff --git a/dotnet/src/Shared/IntegrationTestsAzureCredentials/TestAzureCliCredentials.cs b/dotnet/src/Shared/IntegrationTestsAzureCredentials/TestAzureCliCredentials.cs new file mode 100644 index 0000000000..f1c83ce1f2 --- /dev/null +++ b/dotnet/src/Shared/IntegrationTestsAzureCredentials/TestAzureCliCredentials.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. + +#pragma warning disable IDE0005 // This is required in some projects and not in others. +using System; +#pragma warning restore IDE0005 +using Azure.Identity; + +namespace Shared.IntegrationTests; + +/// +/// Provides credential instances for integration tests with +/// increased timeouts to avoid CI pipeline authentication failures. +/// +internal static class TestAzureCliCredentials +{ + /// + /// The default timeout for Azure CLI credential operations. + /// Increased from the default (~13s) to accommodate CI pipeline latency. + /// + private static readonly TimeSpan s_processTimeout = TimeSpan.FromSeconds(60); + + /// + /// Creates a new with an increased process timeout + /// suitable for CI environments. + /// + public static AzureCliCredential CreateAzureCliCredential() => + new(new AzureCliCredentialOptions { ProcessTimeout = s_processTimeout }); +} diff --git a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientCreateTests.cs b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientCreateTests.cs index ec4103f6a8..a6691a41bd 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientCreateTests.cs +++ b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientCreateTests.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using AgentConformance.IntegrationTests.Support; using Azure.AI.Projects; using Azure.AI.Projects.OpenAI; -using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using OpenAI.Files; @@ -17,7 +16,7 @@ namespace AzureAI.IntegrationTests; public class AIProjectClientCreateTests { - private readonly AIProjectClient _client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), new AzureCliCredential()); + private readonly AIProjectClient _client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), TestAzureCliCredentials.CreateAzureCliCredential()); [Theory] [InlineData("CreateWithChatClientAgentOptionsAsync")] diff --git a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs index 192e8b6c9e..6356bb6e01 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs +++ b/dotnet/tests/AzureAI.IntegrationTests/AIProjectClientFixture.cs @@ -8,7 +8,6 @@ using AgentConformance.IntegrationTests; using AgentConformance.IntegrationTests.Support; using Azure.AI.Projects; using Azure.AI.Projects.OpenAI; -using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using OpenAI.Responses; @@ -170,13 +169,13 @@ public class AIProjectClientFixture : IChatClientAgentFixture public virtual async ValueTask InitializeAsync() { - this._client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), new AzureCliCredential()); + this._client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), TestAzureCliCredentials.CreateAzureCliCredential()); this._agent = await this.CreateChatClientAgentAsync(); } public async Task InitializeAsync(ChatClientAgentOptions options) { - this._client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), new AzureCliCredential()); + this._client = new(new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint)), TestAzureCliCredentials.CreateAzureCliCredential()); this._agent = await this.CreateChatClientAgentAsync(options); } } diff --git a/dotnet/tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj b/dotnet/tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj index 7ea633e3d4..2703360cb2 100644 --- a/dotnet/tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj +++ b/dotnet/tests/AzureAI.IntegrationTests/AzureAI.IntegrationTests.csproj @@ -3,6 +3,7 @@ $(NoWarn);CS8793 True + True diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj index 2b12fe728a..0913d484e5 100644 --- a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj +++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj @@ -3,6 +3,7 @@ $(NoWarn);CS8793 True + True diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs index f750b5a8e7..6b29bb4b08 100644 --- a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs +++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentCreateTests.cs @@ -6,7 +6,6 @@ using System.IO; using System.Threading.Tasks; using AgentConformance.IntegrationTests.Support; using Azure.AI.Agents.Persistent; -using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using Shared.IntegrationTests; @@ -15,7 +14,7 @@ namespace AzureAIAgentsPersistent.IntegrationTests; public class AzureAIAgentsPersistentCreateTests { - private readonly PersistentAgentsClient _persistentAgentsClient = new(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint), new AzureCliCredential()); + private readonly PersistentAgentsClient _persistentAgentsClient = new(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint), TestAzureCliCredentials.CreateAzureCliCredential()); [Theory] [InlineData("CreateWithChatClientAgentOptionsAsync")] diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentFixture.cs b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentFixture.cs index 1ebbd2b4f1..e6446be1cf 100644 --- a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentFixture.cs +++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistentFixture.cs @@ -7,7 +7,6 @@ using AgentConformance.IntegrationTests; using AgentConformance.IntegrationTests.Support; using Azure; using Azure.AI.Agents.Persistent; -using Azure.Identity; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using Shared.IntegrationTests; @@ -99,7 +98,7 @@ public class AzureAIAgentsPersistentFixture : IChatClientAgentFixture public async ValueTask InitializeAsync() { - this._persistentAgentsClient = new(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint), new AzureCliCredential()); + this._persistentAgentsClient = new(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint), TestAzureCliCredentials.CreateAzureCliCredential()); this._agent = await this.CreateChatClientAgentAsync(); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj index ac4f52e3eb..adc184e510 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/Microsoft.Agents.AI.DurableTask.IntegrationTests.csproj @@ -3,6 +3,7 @@ $(TargetFrameworksCore) enable + True diff --git a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TestHelper.cs b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TestHelper.cs index e092db9e8d..d9350cec59 100644 --- a/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TestHelper.cs +++ b/dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/TestHelper.cs @@ -2,7 +2,6 @@ using Azure; using Azure.AI.OpenAI; -using Azure.Identity; using Microsoft.Agents.AI.DurableTask.IntegrationTests.Logging; using Microsoft.DurableTask; using Microsoft.DurableTask.Client; @@ -14,6 +13,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using OpenAI.Chat; +using Shared.IntegrationTests; namespace Microsoft.Agents.AI.DurableTask.IntegrationTests; @@ -165,7 +165,7 @@ internal sealed class TestHelper : IDisposable AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey) ? new AzureOpenAIClient(new Uri(azureOpenAiEndpoint), new AzureKeyCredential(azureOpenAiKey)) - : new AzureOpenAIClient(new Uri(azureOpenAiEndpoint), new AzureCliCredential()); + : new AzureOpenAIClient(new Uri(azureOpenAiEndpoint), TestAzureCliCredentials.CreateAzureCliCredential()); return client.GetChatClient(azureOpenAiDeploymentName); } diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs index 4b1838335c..9b3c95c5c2 100644 --- a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/FoundryMemoryProviderTests.cs @@ -3,7 +3,6 @@ using System; using System.Threading.Tasks; using Azure.AI.Projects; -using Azure.Identity; using Microsoft.Extensions.Configuration; using Shared.IntegrationTests; @@ -41,7 +40,7 @@ public sealed class FoundryMemoryProviderTests : IDisposable if (!string.IsNullOrWhiteSpace(endpoint) && !string.IsNullOrWhiteSpace(memoryStoreName)) { - this._client = new AIProjectClient(new Uri(endpoint), new AzureCliCredential()); + this._client = new AIProjectClient(new Uri(endpoint), TestAzureCliCredentials.CreateAzureCliCredential()); this._memoryStoreName = memoryStoreName; this._deploymentName = deploymentName ?? "gpt-4.1-mini"; } diff --git a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj index 4bf96a5b35..af184142ca 100644 --- a/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests/Microsoft.Agents.AI.FoundryMemory.IntegrationTests.csproj @@ -2,6 +2,7 @@ True + True diff --git a/dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/GitHubCopilotAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/GitHubCopilotAgentTests.cs index 5806636925..52ea0026dc 100644 --- a/dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/GitHubCopilotAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/GitHubCopilotAgentTests.cs @@ -221,4 +221,26 @@ public sealed class GitHubCopilotAgentTests Assert.Null(result.ConfigDir); Assert.True(result.Streaming); } + + [Fact] + public void ConvertToAgentResponseUpdate_AssistantMessageEvent_DoesNotEmitTextContent() + { + var assistantMessage = new AssistantMessageEvent + { + Data = new AssistantMessageData + { + MessageId = "msg-456", + Content = "Some streamed content that was already delivered via delta events" + } + }; + CopilotClient copilotClient = new(new CopilotClientOptions { AutoStart = false }); + const string TestId = "agent-id"; + var agent = new GitHubCopilotAgent(copilotClient, ownsClient: false, id: TestId, tools: null); + AgentResponseUpdate result = agent.ConvertToAgentResponseUpdate(assistantMessage); + + // result.Text need to be empty because the content was already delivered via delta events, and we want to avoid emitting duplicate content in the response update. + // The content should be delivered through TextContent in the Contents collection instead. + Assert.Empty(result.Text); + Assert.DoesNotContain(result.Contents, c => c is TextContent); + } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/FunctionToolAgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/FunctionToolAgentProvider.cs index 8198618b65..98243dc4d3 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/FunctionToolAgentProvider.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/FunctionToolAgentProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Azure.AI.Projects; using Azure.AI.Projects.OpenAI; -using Azure.Identity; using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; using OpenAI.Responses; @@ -25,7 +24,7 @@ internal sealed class FunctionToolAgentProvider(IConfiguration configuration) : AIFunctionFactory.Create(menuPlugin.GetItemPrice), ]; - AIProjectClient aiProjectClient = new(foundryEndpoint, new AzureCliCredential()); + AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential()); yield return await aiProjectClient.CreateAgentAsync( diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MarketingAgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MarketingAgentProvider.cs index f84a40ae23..693d99b638 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MarketingAgentProvider.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MarketingAgentProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Azure.AI.Projects; using Azure.AI.Projects.OpenAI; -using Azure.Identity; using Microsoft.Extensions.Configuration; using Shared.Foundry; using Shared.IntegrationTests; @@ -15,7 +14,7 @@ internal sealed class MarketingAgentProvider(IConfiguration configuration) : Age { protected override async IAsyncEnumerable CreateAgentsAsync(Uri foundryEndpoint) { - AIProjectClient aiProjectClient = new(foundryEndpoint, new AzureCliCredential()); + AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential()); yield return await aiProjectClient.CreateAgentAsync( diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MathChatAgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MathChatAgentProvider.cs index 92cea7d76a..91d63404bd 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MathChatAgentProvider.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/MathChatAgentProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Azure.AI.Projects; using Azure.AI.Projects.OpenAI; -using Azure.Identity; using Microsoft.Extensions.Configuration; using Shared.Foundry; using Shared.IntegrationTests; @@ -15,7 +14,7 @@ internal sealed class MathChatAgentProvider(IConfiguration configuration) : Agen { protected override async IAsyncEnumerable CreateAgentsAsync(Uri foundryEndpoint) { - AIProjectClient aiProjectClient = new(foundryEndpoint, new AzureCliCredential()); + AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential()); yield return await aiProjectClient.CreateAgentAsync( diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/PoemAgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/PoemAgentProvider.cs index 8882709a03..1b79e4e25e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/PoemAgentProvider.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/PoemAgentProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Azure.AI.Projects; using Azure.AI.Projects.OpenAI; -using Azure.Identity; using Microsoft.Extensions.Configuration; using Shared.Foundry; using Shared.IntegrationTests; @@ -15,7 +14,7 @@ internal sealed class PoemAgentProvider(IConfiguration configuration) : AgentPro { protected override async IAsyncEnumerable CreateAgentsAsync(Uri foundryEndpoint) { - AIProjectClient aiProjectClient = new(foundryEndpoint, new AzureCliCredential()); + AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential()); yield return await aiProjectClient.CreateAgentAsync( diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/TestAgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/TestAgentProvider.cs index 03b201d440..dcb09a4798 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/TestAgentProvider.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/TestAgentProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Azure.AI.Projects; using Azure.AI.Projects.OpenAI; -using Azure.Identity; using Microsoft.Extensions.Configuration; using Shared.Foundry; using Shared.IntegrationTests; @@ -15,7 +14,7 @@ internal sealed class TestAgentProvider(IConfiguration configuration) : AgentPro { protected override async IAsyncEnumerable CreateAgentsAsync(Uri foundryEndpoint) { - AIProjectClient aiProjectClient = new(foundryEndpoint, new AzureCliCredential()); + AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential()); yield return await aiProjectClient.CreateAgentAsync( diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/VisionAgentProvider.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/VisionAgentProvider.cs index 1c09ea9247..0d95342264 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/VisionAgentProvider.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Agents/VisionAgentProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Azure.AI.Projects; using Azure.AI.Projects.OpenAI; -using Azure.Identity; using Microsoft.Extensions.Configuration; using Shared.Foundry; using Shared.IntegrationTests; @@ -15,7 +14,7 @@ internal sealed class VisionAgentProvider(IConfiguration configuration) : AgentP { protected override async IAsyncEnumerable CreateAgentsAsync(Uri foundryEndpoint) { - AIProjectClient aiProjectClient = new(foundryEndpoint, new AzureCliCredential()); + AIProjectClient aiProjectClient = new(foundryEndpoint, TestAzureCliCredentials.CreateAzureCliCredential()); yield return await aiProjectClient.CreateAgentAsync( diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs index 562ce8abda..4749289f5a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/AzureAgentProviderTest.cs @@ -2,9 +2,9 @@ using System.Linq; using System.Threading.Tasks; -using Azure.Identity; using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; using Microsoft.Extensions.AI; +using Shared.IntegrationTests; namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests; @@ -14,7 +14,7 @@ public sealed class AzureAgentProviderTest(ITestOutputHelper output) : Integrati public async Task ConversationTestAsync() { // Arrange - AzureAgentProvider provider = new(this.TestEndpoint, new AzureCliCredential()); + AzureAgentProvider provider = new(this.TestEndpoint, TestAzureCliCredentials.CreateAzureCliCredential()); // Act string conversationId = await provider.CreateConversationAsync(); // Assert diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs index 59f2dd51ed..6be840ce48 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Framework/IntegrationTest.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; -using Azure.Identity; using Microsoft.Agents.AI.Workflows.Declarative.PowerFx; using Microsoft.Agents.ObjectModel; using Microsoft.Extensions.AI; @@ -67,7 +66,7 @@ public abstract class IntegrationTest : IDisposable protected async ValueTask CreateOptionsAsync(bool externalConversation, IMcpToolHandler? mcpToolProvider, params IEnumerable functionTools) { AzureAgentProvider agentProvider = - new(this.TestEndpoint, new AzureCliCredential()) + new(this.TestEndpoint, TestAzureCliCredentials.CreateAzureCliCredential()) { Functions = functionTools, }; diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs index 60c4915579..7c3aef758c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/MediaInputTest.cs @@ -4,11 +4,11 @@ using System; using System.IO; using System.Threading.Tasks; using Azure.AI.Projects; -using Azure.Identity; using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Agents; using Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.Framework; using Microsoft.Extensions.AI; using OpenAI.Files; +using Shared.IntegrationTests; namespace Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests; @@ -76,7 +76,7 @@ public sealed class MediaInputTest(ITestOutputHelper output) : IntegrationTest(o { // Arrange byte[] fileData = ReadLocalFile(fileSource); - AIProjectClient client = new(this.TestEndpoint, new AzureCliCredential()); + AIProjectClient client = new(this.TestEndpoint, TestAzureCliCredentials.CreateAzureCliCredential()); using MemoryStream contentStream = new(fileData); OpenAIFileClient fileClient = client.GetProjectOpenAIClient().GetOpenAIFileClient(); OpenAIFile fileInfo = await fileClient.UploadFileAsync(contentStream, documentName, FileUploadPurpose.Assistants); diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj index 92e09fcebb..d37dd58c8c 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests/Microsoft.Agents.AI.Workflows.Declarative.IntegrationTests.csproj @@ -5,6 +5,7 @@ true true true + True diff --git a/python/packages/core/agent_framework/_agents.py b/python/packages/core/agent_framework/_agents.py index cd2dc7bfc7..a0c998757c 100644 --- a/python/packages/core/agent_framework/_agents.py +++ b/python/packages/core/agent_framework/_agents.py @@ -454,6 +454,7 @@ class BaseAgent(SerializationMixin): stream_callback: Callable[[AgentResponseUpdate], None] | Callable[[AgentResponseUpdate], Awaitable[None]] | None = None, + propagate_session: bool = False, ) -> FunctionTool: """Create a FunctionTool that wraps this agent. @@ -464,6 +465,12 @@ class BaseAgent(SerializationMixin): arg_description: The description for the function argument. If None, defaults to "Task for {tool_name}". stream_callback: Optional callback for streaming responses. If provided, uses run(..., stream=True). + propagate_session: If True, the parent agent's ``AgentSession`` is + forwarded to this sub-agent's ``run()`` call, so both agents + operate within the same logical session (sharing the same + ``session_id`` and provider-managed state, such as any stored + conversation history or metadata). Defaults to False, meaning + the sub-agent runs with a new, independent session. Returns: A FunctionTool that can be used as a tool by other agents. @@ -480,9 +487,12 @@ class BaseAgent(SerializationMixin): # Create an agent agent = Agent(client=client, name="research-agent", description="Performs research tasks") - # Convert the agent to a tool + # Convert the agent to a tool (independent session) research_tool = agent.as_tool() + # Convert the agent to a tool (shared session with parent) + research_tool = agent.as_tool(propagate_session=True) + # Use the tool with another agent coordinator = Agent(client=client, name="coordinator", tools=research_tool) """ @@ -509,16 +519,21 @@ class BaseAgent(SerializationMixin): # Extract the input from kwargs using the specified arg_name input_text = kwargs.get(arg_name, "") - # Forward runtime context kwargs, excluding arg_name and conversation_id. - forwarded_kwargs = {k: v for k, v in kwargs.items() if k not in (arg_name, "conversation_id", "options")} + # Extract parent session when propagate_session is enabled + parent_session = kwargs.get("session") if propagate_session else None + + # Forward runtime context kwargs, excluding framework-internal keys. + forwarded_kwargs = { + k: v for k, v in kwargs.items() if k not in (arg_name, "conversation_id", "options", "session") + } if stream_callback is None: # Use non-streaming mode - return (await self.run(input_text, stream=False, **forwarded_kwargs)).text + return (await self.run(input_text, stream=False, session=parent_session, **forwarded_kwargs)).text # Use streaming mode - accumulate updates and create final response response_updates: list[AgentResponseUpdate] = [] - async for update in self.run(input_text, stream=True, **forwarded_kwargs): + async for update in self.run(input_text, stream=True, session=parent_session, **forwarded_kwargs): response_updates.append(update) if is_async_callback: await stream_callback(update) # type: ignore[misc] @@ -1061,6 +1076,9 @@ class RawAgent(BaseAgent, Generic[OptionsCoT]): # type: ignore[misc] # in function middleware context and tool invocation. existing_additional_args = opts.pop("additional_function_arguments", None) or {} additional_function_arguments = {**kwargs, **existing_additional_args} + # Include session so as_tool() wrappers with propagate_session=True can access it. + if active_session is not None: + additional_function_arguments["session"] = active_session # Build options dict from run() options merged with provided options run_opts: dict[str, Any] = { diff --git a/python/packages/core/tests/core/test_agents.py b/python/packages/core/tests/core/test_agents.py index c8d2d9bf8b..d41b87b707 100644 --- a/python/packages/core/tests/core/test_agents.py +++ b/python/packages/core/tests/core/test_agents.py @@ -707,6 +707,81 @@ async def test_chat_agent_as_tool_name_sanitization(client: SupportsChatGetRespo assert tool.name == expected_tool_name, f"Expected {expected_tool_name}, got {tool.name} for input {agent_name}" +async def test_chat_agent_as_tool_propagate_session_true(client: SupportsChatGetResponse) -> None: + """Test that propagate_session=True forwards the parent's session to the sub-agent.""" + agent = Agent(client=client, name="SubAgent", description="Sub agent") + tool = agent.as_tool(propagate_session=True) + + parent_session = AgentSession(session_id="parent-session-123") + parent_session.state["shared_key"] = "shared_value" + + # Spy on the agent's run method to capture the session argument + original_run = agent.run + captured_session = None + + def capturing_run(*args: Any, **kwargs: Any) -> Any: + nonlocal captured_session + captured_session = kwargs.get("session") + return original_run(*args, **kwargs) + + agent.run = capturing_run # type: ignore[assignment, method-assign] + + await tool.invoke(arguments=tool.input_model(task="Hello"), session=parent_session) + + assert captured_session is parent_session + assert captured_session.session_id == "parent-session-123" + assert captured_session.state["shared_key"] == "shared_value" + + +async def test_chat_agent_as_tool_propagate_session_false_by_default(client: SupportsChatGetResponse) -> None: + """Test that propagate_session defaults to False and does not forward the session.""" + agent = Agent(client=client, name="SubAgent", description="Sub agent") + tool = agent.as_tool() # default: propagate_session=False + + parent_session = AgentSession(session_id="parent-session-456") + + original_run = agent.run + captured_session = None + + def capturing_run(*args: Any, **kwargs: Any) -> Any: + nonlocal captured_session + captured_session = kwargs.get("session") + return original_run(*args, **kwargs) + + agent.run = capturing_run # type: ignore[assignment, method-assign] + + await tool.invoke(arguments=tool.input_model(task="Hello"), session=parent_session) + + assert captured_session is None + + +async def test_chat_agent_as_tool_propagate_session_shares_state(client: SupportsChatGetResponse) -> None: + """Test that shared session allows the sub-agent to read and write parent's state.""" + agent = Agent(client=client, name="SubAgent", description="Sub agent") + tool = agent.as_tool(propagate_session=True) + + parent_session = AgentSession(session_id="shared-session") + parent_session.state["counter"] = 0 + + # The sub-agent receives the same session object, so mutations are shared + original_run = agent.run + captured_session = None + + def capturing_run(*args: Any, **kwargs: Any) -> Any: + nonlocal captured_session + captured_session = kwargs.get("session") + if captured_session: + captured_session.state["counter"] += 1 + return original_run(*args, **kwargs) + + agent.run = capturing_run # type: ignore[assignment, method-assign] + + await tool.invoke(arguments=tool.input_model(task="Hello"), session=parent_session) + + # The parent's state should reflect the sub-agent's mutation + assert parent_session.state["counter"] == 1 + + async def test_chat_agent_as_mcp_server_basic(client: SupportsChatGetResponse) -> None: """Test basic as_mcp_server functionality.""" agent = Agent(client=client, name="TestAgent", description="Test agent for MCP") diff --git a/python/samples/02-agents/tools/agent_as_tool_with_session_propagation.py b/python/samples/02-agents/tools/agent_as_tool_with_session_propagation.py new file mode 100644 index 0000000000..33748437e0 --- /dev/null +++ b/python/samples/02-agents/tools/agent_as_tool_with_session_propagation.py @@ -0,0 +1,93 @@ +# Copyright (c) Microsoft. All rights reserved. + +import asyncio +from collections.abc import Awaitable, Callable + +from agent_framework import AgentContext, AgentSession +from agent_framework.openai import OpenAIResponsesClient +from dotenv import load_dotenv + +load_dotenv() + +""" +Agent-as-Tool: Session Propagation Example + +Demonstrates how to share an AgentSession between a coordinator agent and a +sub-agent invoked as a tool using ``propagate_session=True``. + +When session propagation is enabled, both agents share the same session object, +including session_id and the mutable state dict. This allows correlated +conversation tracking and shared state across the agent hierarchy. + +The middleware functions below are purely for observability — they are NOT +required for session propagation to work. +""" + + +async def log_session( + context: AgentContext, + call_next: Callable[[], Awaitable[None]], +) -> None: + """Agent middleware that logs the session received by each agent. + + NOT required for session propagation — only used to observe the flow. + If propagation is working, both agents will show the same session_id. + """ + session: AgentSession | None = context.session + agent_name = context.agent.name or "unknown" + session_id = session.session_id if session else None + state = dict(session.state) if session else {} + print(f" [{agent_name}] session_id={session_id}, state={state}") + await call_next() + + +async def main() -> None: + print("=== Agent-as-Tool: Session Propagation ===\n") + + client = OpenAIResponsesClient() + + # --- Sub-agent: a research specialist --- + # The sub-agent has the same log_session middleware to prove it receives the session. + research_agent = client.as_agent( + name="ResearchAgent", + instructions="You are a research assistant. Provide concise answers.", + middleware=[log_session], + ) + + # propagate_session=True: the coordinator's session will be forwarded + research_tool = research_agent.as_tool( + name="research", + description="Research a topic and return findings", + arg_name="query", + arg_description="The research query", + propagate_session=True, + ) + + # --- Coordinator agent --- + coordinator = client.as_agent( + name="CoordinatorAgent", + instructions="You coordinate research. Use the 'research' tool to look up information.", + tools=[research_tool], + middleware=[log_session], + ) + + # Create a shared session and put some state in it + session = coordinator.create_session() + session.state["request_source"] = "demo" + print(f"Session ID: {session.session_id}") + print(f"Session state before run: {session.state}\n") + + query = "What are the latest developments in quantum computing?" + print(f"User: {query}\n") + + result = await coordinator.run(query, session=session) + + print(f"\nCoordinator: {result}\n") + print(f"Session state after run: {session.state}") + print( + "\nIf both agents show the same session_id above, session propagation is working." + ) + + +if __name__ == "__main__": + asyncio.run(main())