diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows/AIAgentHostOptions.cs b/dotnet/src/Microsoft.Agents.AI.Workflows/AIAgentHostOptions.cs
index c981b5d8015..0e4d2e0f5b3 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 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 currentTurnMessages,
+ IReadOnlyList forwardableMessages)
+ {
+ this.ExecutorId = Throw.IfNull(executorId);
+ this.AgentResponse = Throw.IfNull(agentResponse);
+ this.CurrentTurnMessages = new List(Throw.IfNull(currentTurnMessages));
+ 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 messages for the current host turn, including the input messages and forwarded response messages.
+ ///
+ public IReadOnlyList CurrentTurnMessages { 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..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
@@ -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! 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.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 ab058de62d4..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
@@ -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! 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.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 ab058de62d4..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
@@ -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! 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.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 ab058de62d4..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
@@ -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! 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.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 ab058de62d4..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
@@ -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! 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.CurrentTurnMessages.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..1a77a2d83d6 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentHostExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows/Specialized/AIAgentHostExecutor.cs
@@ -69,8 +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));
+ 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)
@@ -201,6 +205,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/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/AIAgentHostExecutorTests.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AIAgentHostExecutorTests.cs
index 2818d230326..699a6439832 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AIAgentHostExecutorTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/AIAgentHostExecutorTests.cs
@@ -134,6 +134,109 @@ 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