From d6746622fa13ac2eb39a9e7e694378d70a5f5e51 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 31 Aug 2026 15:31:42 -0400 Subject: [PATCH 1/4] feat(dotnet): forward agent response details in workflows Add an opt-in AIAgentHostResponse workflow message that carries the complete AgentResponse, portable full conversation, and sanitized forwarded messages from AIAgentHostExecutor. This lets downstream custom executors inspect response metadata such as finish reason and usage without changing the default chat-message forwarding behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 41390b8c-4c07-493a-bb2d-d1a087bd94bc --- .../AIAgentHostOptions.cs | 63 +++++++++++ .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 8 ++ .../PublicAPI/net472/PublicAPI.Unshipped.txt | 8 ++ .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 8 ++ .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 8 ++ .../netstandard2.0/PublicAPI.Unshipped.txt | 8 ++ .../Specialized/AIAgentHostExecutor.cs | 14 ++- .../AIAgentHostExecutorTests.cs | 102 ++++++++++++++++++ 8 files changed, 218 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/AIAgentHostOptions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/AIAgentHostOptions.cs index c981b5d8015..388dd4edcce 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/AIAgentHostOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/AIAgentHostOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI.Workflows; @@ -44,4 +46,65 @@ public sealed class AIAgentHostOptions /// by the agent during its turn. /// public bool ForwardIncomingMessages { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the complete agent response should be forwarded as a workflow message. + /// + /// + /// When enabled, downstream executors that handle can inspect response-level + /// metadata such as , , and + /// . Existing chat-message forwarding is unchanged. + /// + public bool ForwardAgentResponse { get; set; } +} + +/// +/// Represents the complete response produced by an hosted in a workflow. +/// +/// +/// is sent as a workflow message when +/// is enabled. It allows custom downstream executors to inspect +/// response-level metadata while the existing chat-message path continues to carry portable conversation messages to +/// chat-protocol executors. +/// +public sealed class AIAgentHostResponse +{ + /// + /// Initializes a new instance of the class. + /// + /// The ID of the executor that produced the response. + /// The complete response returned by the hosted agent. + /// The portable conversation context containing input messages and forwarded response messages. + /// The sanitized response messages forwarded on the chat-message path. + public AIAgentHostResponse( + string executorId, + AgentResponse agentResponse, + IReadOnlyList fullConversation, + IReadOnlyList forwardableMessages) + { + this.ExecutorId = Throw.IfNull(executorId); + this.AgentResponse = Throw.IfNull(agentResponse); + this.FullConversation = new List(Throw.IfNull(fullConversation)); + this.ForwardableMessages = new List(Throw.IfNull(forwardableMessages)); + } + + /// + /// Gets the ID of the executor that produced the response. + /// + public string ExecutorId { get; } + + /// + /// Gets the complete response returned by the hosted agent. + /// + public AgentResponse AgentResponse { get; } + + /// + /// Gets the portable conversation context containing the input messages and forwarded response messages. + /// + public IReadOnlyList FullConversation { get; } + + /// + /// Gets the sanitized response messages forwarded on the chat-message path. + /// + public IReadOnlyList ForwardableMessages { get; } } diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net10.0/PublicAPI.Unshipped.txt index ab058de62d4..6b281afa7c9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -1 +1,9 @@ #nullable enable +Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.get -> bool +Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.set -> void +Microsoft.Agents.AI.Workflows.AIAgentHostResponse +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AgentResponse.get -> Microsoft.Agents.AI.AgentResponse! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AIAgentHostResponse(string! executorId, Microsoft.Agents.AI.AgentResponse! agentResponse, System.Collections.Generic.IReadOnlyList! fullConversation, System.Collections.Generic.IReadOnlyList! forwardableMessages) -> void +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ExecutorId.get -> string! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ForwardableMessages.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.FullConversation.get -> System.Collections.Generic.IReadOnlyList! diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net472/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net472/PublicAPI.Unshipped.txt index ab058de62d4..6b281afa7c9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -1 +1,9 @@ #nullable enable +Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.get -> bool +Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.set -> void +Microsoft.Agents.AI.Workflows.AIAgentHostResponse +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AgentResponse.get -> Microsoft.Agents.AI.AgentResponse! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AIAgentHostResponse(string! executorId, Microsoft.Agents.AI.AgentResponse! agentResponse, System.Collections.Generic.IReadOnlyList! fullConversation, System.Collections.Generic.IReadOnlyList! forwardableMessages) -> void +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ExecutorId.get -> string! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ForwardableMessages.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.FullConversation.get -> System.Collections.Generic.IReadOnlyList! diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net8.0/PublicAPI.Unshipped.txt index ab058de62d4..6b281afa7c9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1 +1,9 @@ #nullable enable +Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.get -> bool +Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.set -> void +Microsoft.Agents.AI.Workflows.AIAgentHostResponse +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AgentResponse.get -> Microsoft.Agents.AI.AgentResponse! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AIAgentHostResponse(string! executorId, Microsoft.Agents.AI.AgentResponse! agentResponse, System.Collections.Generic.IReadOnlyList! fullConversation, System.Collections.Generic.IReadOnlyList! forwardableMessages) -> void +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ExecutorId.get -> string! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ForwardableMessages.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.FullConversation.get -> System.Collections.Generic.IReadOnlyList! diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net9.0/PublicAPI.Unshipped.txt index ab058de62d4..6b281afa7c9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -1 +1,9 @@ #nullable enable +Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.get -> bool +Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.set -> void +Microsoft.Agents.AI.Workflows.AIAgentHostResponse +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AgentResponse.get -> Microsoft.Agents.AI.AgentResponse! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AIAgentHostResponse(string! executorId, Microsoft.Agents.AI.AgentResponse! agentResponse, System.Collections.Generic.IReadOnlyList! fullConversation, System.Collections.Generic.IReadOnlyList! forwardableMessages) -> void +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ExecutorId.get -> string! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ForwardableMessages.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.FullConversation.get -> System.Collections.Generic.IReadOnlyList! diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index ab058de62d4..6b281afa7c9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1 +1,9 @@ #nullable enable +Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.get -> bool +Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.set -> void +Microsoft.Agents.AI.Workflows.AIAgentHostResponse +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AgentResponse.get -> Microsoft.Agents.AI.AgentResponse! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AIAgentHostResponse(string! executorId, Microsoft.Agents.AI.AgentResponse! agentResponse, System.Collections.Generic.IReadOnlyList! fullConversation, System.Collections.Generic.IReadOnlyList! forwardableMessages) -> void +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ExecutorId.get -> string! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ForwardableMessages.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.FullConversation.get -> System.Collections.Generic.IReadOnlyList! diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentHostExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentHostExecutor.cs index 85ff6e40528..ae4bfb3dc3e 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentHostExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentHostExecutor.cs @@ -70,7 +70,8 @@ private ProtocolBuilder ConfigureUserInputHandling(ProtocolBuilder protocolBuild protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder) { return this.ConfigureUserInputHandling(base.ConfigureProtocol(protocolBuilder)) - .ConfigureRoutes(routeBuilder => routeBuilder.AddHandler(this.ResetChat)); + .ConfigureRoutes(routeBuilder => routeBuilder.AddHandler(this.ResetChat)) + .SendsMessage(); } internal void ResetChat(ResetChatSignal signal, IWorkflowContext context) @@ -201,6 +202,17 @@ await context.SendMessageAsync(forwardableMessages, cancellationToken) .ConfigureAwait(false); } + if (this._options.ForwardAgentResponse) + { + await context.SendMessageAsync( + new AIAgentHostResponse( + this.Id, + response, + [.. messages, .. forwardableMessages], + forwardableMessages), + cancellationToken).ConfigureAwait(false); + } + // If we have no outstanding requests, we can yield a turn token back to the workflow. if (!this.HasOutstandingRequests) { diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AIAgentHostExecutorTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AIAgentHostExecutorTests.cs index 2818d230326..6a934953f7f 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AIAgentHostExecutorTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AIAgentHostExecutorTests.cs @@ -134,6 +134,108 @@ protected override async IAsyncEnumerable RunCoreStreamingA } } + [Fact] + public async Task Test_AgentHostExecutor_ForwardsAgentResponseMessageAsync() + { + // Arrange + ChatMessage userMessage = new(ChatRole.User, "Summarize this.") { AuthorName = "User" }; + ChatMessage responseMessage = new(ChatRole.Assistant, [new TextContent("Partial answer")]) + { + AuthorName = TestAgentName, + MessageId = "message-id", + RawRepresentation = "provider-message", + }; + ChatMessage reasoningMessage = new(ChatRole.Assistant, [new TextReasoningContent("internal reasoning")]) + { + AuthorName = TestAgentName, + MessageId = "reasoning-id", + RawRepresentation = "provider-reasoning", + }; + AgentResponse agentResponse = new([responseMessage, reasoningMessage]) + { + AgentId = TestAgentId, + ResponseId = "response-id", + FinishReason = ChatFinishReason.Length, + Usage = new UsageDetails { InputTokenCount = 10, OutputTokenCount = 2, TotalTokenCount = 12 }, + AdditionalProperties = new() { ["detail"] = "metadata" }, + }; + AIAgentHostExecutor executor = new(new FixedResponseAgent(agentResponse, TestAgentId, TestAgentName), new() { ForwardAgentResponse = true }); + TestRunContext testContext = new(); + testContext.ConfigureExecutor(executor); + + // Act + await executor.Router.RouteMessageAsync(userMessage, testContext.BindWorkflowContext(executor.Id)); + await executor.TakeTurnAsync(new(), testContext.BindWorkflowContext(executor.Id)); + + // Assert + Assert.Contains(typeof(AIAgentHostResponse), executor.Protocol.Describe().Sends); + + IEnumerable sentMessages = testContext.QueuedMessages[executor.Id].Select(envelope => envelope.Message); + AIAgentHostResponse hostResponse = Assert.Single(sentMessages.OfType()); + + Assert.Equal(executor.Id, hostResponse.ExecutorId); + Assert.Same(agentResponse, hostResponse.AgentResponse); + Assert.Equal(ChatFinishReason.Length, hostResponse.AgentResponse.FinishReason); + Assert.Equal("response-id", hostResponse.AgentResponse.ResponseId); + Assert.Equal(12, hostResponse.AgentResponse.Usage?.TotalTokenCount); + Assert.Equal("metadata", hostResponse.AgentResponse.AdditionalProperties?["detail"]); + + ChatMessage forwardableMessage = Assert.Single(hostResponse.ForwardableMessages); + Assert.Equal("Partial answer", forwardableMessage.Text); + Assert.Null(forwardableMessage.RawRepresentation); + + Assert.Equal(2, hostResponse.FullConversation.Count); + Assert.Equal("Summarize this.", hostResponse.FullConversation[0].Text); + Assert.Equal("Partial answer", hostResponse.FullConversation[1].Text); + } + + [Fact] + public async Task Test_AgentHostExecutor_DoesNotForwardAgentResponseMessageByDefaultAsync() + { + // Arrange + AgentResponse agentResponse = new(new ChatMessage(ChatRole.Assistant, "Hello")); + AIAgentHostExecutor executor = new(new FixedResponseAgent(agentResponse, TestAgentId, TestAgentName), new()); + TestRunContext testContext = new(); + testContext.ConfigureExecutor(executor); + + // Act + await executor.TakeTurnAsync(new(), testContext.BindWorkflowContext(executor.Id)); + + // Assert + Assert.Contains(executor.Id, testContext.QueuedMessages); + Assert.DoesNotContain(testContext.QueuedMessages[executor.Id], envelope => envelope.Message is AIAgentHostResponse); + } + + private sealed class FixedResponseAgent(AgentResponse response, string? id = null, string? name = null) : AIAgent + { + protected override string? IdCore => id; + public override string? Name => name; + + protected override ValueTask CreateSessionCoreAsync(CancellationToken cancellationToken = default) + => new(new FixedResponseSession()); + + protected override ValueTask DeserializeSessionCoreAsync(JsonElement serializedState, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) + => new(new FixedResponseSession()); + + protected override ValueTask SerializeSessionCoreAsync(AgentSession session, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default) + => default; + + protected override Task RunCoreAsync(IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, CancellationToken cancellationToken = default) + => Task.FromResult(response); + + protected override async IAsyncEnumerable RunCoreStreamingAsync(IEnumerable messages, AgentSession? session = null, AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach (AgentResponseUpdate update in response.ToAgentResponseUpdates()) + { + yield return update; + } + + await Task.CompletedTask; + } + + private sealed class FixedResponseSession : AgentSession; + } + [Theory] [InlineData(true, true, false, false)] [InlineData(true, true, false, true)] From 5779b701619a87c7cadd8895f812a63db75c14f3 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 1 Sep 2026 07:19:48 -0400 Subject: [PATCH 2/4] fix(dotnet): register agent host response JSON contract Add AIAgentHostResponse to the workflows source-generated JSON context so PortableValue checkpoint serialization can resolve the framework-owned message envelope without reflection serialization. Cover the checkpoint-style PortableMessageEnvelope round trip for the new response envelope. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 41390b8c-4c07-493a-bb2d-d1a087bd94bc --- .../WorkflowsJsonUtilities.cs | 1 + .../JsonSerializationTests.cs | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowsJsonUtilities.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowsJsonUtilities.cs index a1d24a95222..1c4ca5eed05 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowsJsonUtilities.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowsJsonUtilities.cs @@ -91,6 +91,7 @@ private static JsonSerializerOptions CreateDefaultOptions() // Message Types [JsonSerializable(typeof(ChatMessage))] + [JsonSerializable(typeof(AIAgentHostResponse))] [JsonSerializable(typeof(ExternalRequest))] [JsonSerializable(typeof(ExternalResponse))] [JsonSerializable(typeof(TurnToken))] diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/JsonSerializationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/JsonSerializationTests.cs index 580ff8209db..a81c6eeaa8e 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/JsonSerializationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/JsonSerializationTests.cs @@ -444,6 +444,37 @@ public void Test_PortableMessageEnvelope_JsonRoundtrip_InternalType() AssertMatches(reconstructedMessage, message.CreateValidatorCheckingText()); } + [Fact] + public void Test_PortableMessageEnvelope_JsonRoundtrip_AIAgentHostResponse() + { + AIAgentHostResponse message = new( + "Source1", + new AgentResponse(new ChatMessage(ChatRole.Assistant, "Hello from agent")) + { + FinishReason = ChatFinishReason.Length, + ResponseId = "response-id", + Usage = new UsageDetails { InputTokenCount = 10, OutputTokenCount = 2, TotalTokenCount = 12 }, + }, + [new ChatMessage(ChatRole.User, "Hello"), new ChatMessage(ChatRole.Assistant, "Hello from agent")], + [new ChatMessage(ChatRole.Assistant, "Hello from agent")]); + + MessageEnvelope envelope = new(message, "Source1", new TypeId(typeof(AIAgentHostResponse)), targetId: "Target1"); + PortableMessageEnvelope value = new(envelope); + PortableMessageEnvelope result = RunJsonRoundtrip(value); + + MessageEnvelope reconstructed = result.ToMessageEnvelope(); + AIAgentHostResponse? reconstructedMessage = ((PortableValue)reconstructed.Message).As(); + + Assert.NotNull(reconstructedMessage); + Assert.Equal(message.ExecutorId, reconstructedMessage.ExecutorId); + Assert.Equal(message.AgentResponse.Text, reconstructedMessage.AgentResponse.Text); + Assert.Equal(message.AgentResponse.FinishReason, reconstructedMessage.AgentResponse.FinishReason); + Assert.Equal(message.AgentResponse.ResponseId, reconstructedMessage.AgentResponse.ResponseId); + Assert.Equal(message.AgentResponse.Usage?.TotalTokenCount, reconstructedMessage.AgentResponse.Usage?.TotalTokenCount); + Assert.Equal(message.FullConversation.Select(m => m.Text), reconstructedMessage.FullConversation.Select(m => m.Text)); + Assert.Equal(message.ForwardableMessages.Select(m => m.Text), reconstructedMessage.ForwardableMessages.Select(m => m.Text)); + } + [Fact] public void Test_PortableMessageEnvelope_JsonRoundtrip_CustomType() { From 6b2cb855fd3c43a6fbfea7fce7e228b8831b604c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:31:27 +0000 Subject: [PATCH 3/4] fix(dotnet): conditionally advertise agent host response Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../Specialized/AIAgentHostExecutor.cs | 9 ++++++--- .../AIAgentHostExecutorTests.cs | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentHostExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentHostExecutor.cs index ae4bfb3dc3e..1a77a2d83d6 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentHostExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentHostExecutor.cs @@ -69,9 +69,12 @@ private ProtocolBuilder ConfigureUserInputHandling(ProtocolBuilder protocolBuild protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder protocolBuilder) { - return this.ConfigureUserInputHandling(base.ConfigureProtocol(protocolBuilder)) - .ConfigureRoutes(routeBuilder => routeBuilder.AddHandler(this.ResetChat)) - .SendsMessage(); + protocolBuilder = this.ConfigureUserInputHandling(base.ConfigureProtocol(protocolBuilder)) + .ConfigureRoutes(routeBuilder => routeBuilder.AddHandler(this.ResetChat)); + + return this._options.ForwardAgentResponse + ? protocolBuilder.SendsMessage() + : protocolBuilder; } internal void ResetChat(ResetChatSignal signal, IWorkflowContext context) diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AIAgentHostExecutorTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AIAgentHostExecutorTests.cs index 6a934953f7f..856e73f1859 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AIAgentHostExecutorTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AIAgentHostExecutorTests.cs @@ -202,6 +202,7 @@ public async Task Test_AgentHostExecutor_DoesNotForwardAgentResponseMessageByDef await executor.TakeTurnAsync(new(), testContext.BindWorkflowContext(executor.Id)); // Assert + Assert.DoesNotContain(typeof(AIAgentHostResponse), executor.Protocol.Describe().Sends); Assert.Contains(executor.Id, testContext.QueuedMessages); Assert.DoesNotContain(testContext.QueuedMessages[executor.Id], envelope => envelope.Message is AIAgentHostResponse); } From e005213b6ecae64769d751870595e869e672946e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 1 Sep 2026 17:21:55 +0000 Subject: [PATCH 4/4] Rename host response turn messages Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../AIAgentHostOptions.cs | 10 +++++----- .../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 4 ++-- .../PublicAPI/net472/PublicAPI.Unshipped.txt | 4 ++-- .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 4 ++-- .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 4 ++-- .../PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt | 4 ++-- .../AIAgentHostExecutorTests.cs | 6 +++--- .../JsonSerializationTests.cs | 2 +- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/AIAgentHostOptions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/AIAgentHostOptions.cs index 388dd4edcce..0e4d2e0f5b3 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/AIAgentHostOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/AIAgentHostOptions.cs @@ -74,17 +74,17 @@ public sealed class AIAgentHostResponse /// /// The ID of the executor that produced the response. /// The complete response returned by the hosted agent. - /// The portable conversation context containing input messages and forwarded response messages. + /// The portable messages for the current host turn, including input messages and forwarded response messages. /// The sanitized response messages forwarded on the chat-message path. public AIAgentHostResponse( string executorId, AgentResponse agentResponse, - IReadOnlyList fullConversation, + IReadOnlyList currentTurnMessages, IReadOnlyList forwardableMessages) { this.ExecutorId = Throw.IfNull(executorId); this.AgentResponse = Throw.IfNull(agentResponse); - this.FullConversation = new List(Throw.IfNull(fullConversation)); + this.CurrentTurnMessages = new List(Throw.IfNull(currentTurnMessages)); this.ForwardableMessages = new List(Throw.IfNull(forwardableMessages)); } @@ -99,9 +99,9 @@ public AIAgentHostResponse( public AgentResponse AgentResponse { get; } /// - /// Gets the portable conversation context containing the input messages and forwarded response messages. + /// Gets the portable messages for the current host turn, including the input messages and forwarded response messages. /// - public IReadOnlyList FullConversation { get; } + public IReadOnlyList CurrentTurnMessages { get; } /// /// Gets the sanitized response messages forwarded on the chat-message path. diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net10.0/PublicAPI.Unshipped.txt index 6b281afa7c9..4dc1b2d2755 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -3,7 +3,7 @@ Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.get -> boo Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.set -> void Microsoft.Agents.AI.Workflows.AIAgentHostResponse Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AgentResponse.get -> Microsoft.Agents.AI.AgentResponse! -Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AIAgentHostResponse(string! executorId, Microsoft.Agents.AI.AgentResponse! agentResponse, System.Collections.Generic.IReadOnlyList! fullConversation, System.Collections.Generic.IReadOnlyList! forwardableMessages) -> void +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AIAgentHostResponse(string! executorId, Microsoft.Agents.AI.AgentResponse! agentResponse, System.Collections.Generic.IReadOnlyList! currentTurnMessages, System.Collections.Generic.IReadOnlyList! forwardableMessages) -> void Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ExecutorId.get -> string! Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ForwardableMessages.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.Agents.AI.Workflows.AIAgentHostResponse.FullConversation.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.CurrentTurnMessages.get -> System.Collections.Generic.IReadOnlyList! diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net472/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net472/PublicAPI.Unshipped.txt index 6b281afa7c9..4dc1b2d2755 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -3,7 +3,7 @@ Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.get -> boo Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.set -> void Microsoft.Agents.AI.Workflows.AIAgentHostResponse Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AgentResponse.get -> Microsoft.Agents.AI.AgentResponse! -Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AIAgentHostResponse(string! executorId, Microsoft.Agents.AI.AgentResponse! agentResponse, System.Collections.Generic.IReadOnlyList! fullConversation, System.Collections.Generic.IReadOnlyList! forwardableMessages) -> void +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AIAgentHostResponse(string! executorId, Microsoft.Agents.AI.AgentResponse! agentResponse, System.Collections.Generic.IReadOnlyList! currentTurnMessages, System.Collections.Generic.IReadOnlyList! forwardableMessages) -> void Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ExecutorId.get -> string! Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ForwardableMessages.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.Agents.AI.Workflows.AIAgentHostResponse.FullConversation.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.CurrentTurnMessages.get -> System.Collections.Generic.IReadOnlyList! diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net8.0/PublicAPI.Unshipped.txt index 6b281afa7c9..4dc1b2d2755 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -3,7 +3,7 @@ Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.get -> boo Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.set -> void Microsoft.Agents.AI.Workflows.AIAgentHostResponse Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AgentResponse.get -> Microsoft.Agents.AI.AgentResponse! -Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AIAgentHostResponse(string! executorId, Microsoft.Agents.AI.AgentResponse! agentResponse, System.Collections.Generic.IReadOnlyList! fullConversation, System.Collections.Generic.IReadOnlyList! forwardableMessages) -> void +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AIAgentHostResponse(string! executorId, Microsoft.Agents.AI.AgentResponse! agentResponse, System.Collections.Generic.IReadOnlyList! currentTurnMessages, System.Collections.Generic.IReadOnlyList! forwardableMessages) -> void Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ExecutorId.get -> string! Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ForwardableMessages.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.Agents.AI.Workflows.AIAgentHostResponse.FullConversation.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.CurrentTurnMessages.get -> System.Collections.Generic.IReadOnlyList! diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net9.0/PublicAPI.Unshipped.txt index 6b281afa7c9..4dc1b2d2755 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -3,7 +3,7 @@ Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.get -> boo Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.set -> void Microsoft.Agents.AI.Workflows.AIAgentHostResponse Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AgentResponse.get -> Microsoft.Agents.AI.AgentResponse! -Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AIAgentHostResponse(string! executorId, Microsoft.Agents.AI.AgentResponse! agentResponse, System.Collections.Generic.IReadOnlyList! fullConversation, System.Collections.Generic.IReadOnlyList! forwardableMessages) -> void +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AIAgentHostResponse(string! executorId, Microsoft.Agents.AI.AgentResponse! agentResponse, System.Collections.Generic.IReadOnlyList! currentTurnMessages, System.Collections.Generic.IReadOnlyList! forwardableMessages) -> void Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ExecutorId.get -> string! Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ForwardableMessages.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.Agents.AI.Workflows.AIAgentHostResponse.FullConversation.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.CurrentTurnMessages.get -> System.Collections.Generic.IReadOnlyList! diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 6b281afa7c9..4dc1b2d2755 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/dotnet/src/Microsoft.Agents.AI.Workflows/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -3,7 +3,7 @@ Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.get -> boo Microsoft.Agents.AI.Workflows.AIAgentHostOptions.ForwardAgentResponse.set -> void Microsoft.Agents.AI.Workflows.AIAgentHostResponse Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AgentResponse.get -> Microsoft.Agents.AI.AgentResponse! -Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AIAgentHostResponse(string! executorId, Microsoft.Agents.AI.AgentResponse! agentResponse, System.Collections.Generic.IReadOnlyList! fullConversation, System.Collections.Generic.IReadOnlyList! forwardableMessages) -> void +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.AIAgentHostResponse(string! executorId, Microsoft.Agents.AI.AgentResponse! agentResponse, System.Collections.Generic.IReadOnlyList! currentTurnMessages, System.Collections.Generic.IReadOnlyList! forwardableMessages) -> void Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ExecutorId.get -> string! Microsoft.Agents.AI.Workflows.AIAgentHostResponse.ForwardableMessages.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.Agents.AI.Workflows.AIAgentHostResponse.FullConversation.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.Agents.AI.Workflows.AIAgentHostResponse.CurrentTurnMessages.get -> System.Collections.Generic.IReadOnlyList! diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AIAgentHostExecutorTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AIAgentHostExecutorTests.cs index 856e73f1859..699a6439832 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AIAgentHostExecutorTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AIAgentHostExecutorTests.cs @@ -184,9 +184,9 @@ public async Task Test_AgentHostExecutor_ForwardsAgentResponseMessageAsync() Assert.Equal("Partial answer", forwardableMessage.Text); Assert.Null(forwardableMessage.RawRepresentation); - Assert.Equal(2, hostResponse.FullConversation.Count); - Assert.Equal("Summarize this.", hostResponse.FullConversation[0].Text); - Assert.Equal("Partial answer", hostResponse.FullConversation[1].Text); + Assert.Equal(2, hostResponse.CurrentTurnMessages.Count); + Assert.Equal("Summarize this.", hostResponse.CurrentTurnMessages[0].Text); + Assert.Equal("Partial answer", hostResponse.CurrentTurnMessages[1].Text); } [Fact] diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/JsonSerializationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/JsonSerializationTests.cs index a81c6eeaa8e..e186bc36f63 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/JsonSerializationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/JsonSerializationTests.cs @@ -471,7 +471,7 @@ public void Test_PortableMessageEnvelope_JsonRoundtrip_AIAgentHostResponse() Assert.Equal(message.AgentResponse.FinishReason, reconstructedMessage.AgentResponse.FinishReason); Assert.Equal(message.AgentResponse.ResponseId, reconstructedMessage.AgentResponse.ResponseId); Assert.Equal(message.AgentResponse.Usage?.TotalTokenCount, reconstructedMessage.AgentResponse.Usage?.TotalTokenCount); - Assert.Equal(message.FullConversation.Select(m => m.Text), reconstructedMessage.FullConversation.Select(m => m.Text)); + Assert.Equal(message.CurrentTurnMessages.Select(m => m.Text), reconstructedMessage.CurrentTurnMessages.Select(m => m.Text)); Assert.Equal(message.ForwardableMessages.Select(m => m.Text), reconstructedMessage.ForwardableMessages.Select(m => m.Text)); }