diff --git a/src/Observability/Hosting/Middleware/BaggageTurnMiddleware.cs b/src/Observability/Hosting/Middleware/BaggageTurnMiddleware.cs
new file mode 100644
index 00000000..9d5111a9
--- /dev/null
+++ b/src/Observability/Hosting/Middleware/BaggageTurnMiddleware.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.A365.Observability.Hosting.Extensions;
+using Microsoft.Agents.A365.Observability.Runtime.Common;
+using Microsoft.Agents.Builder;
+using Microsoft.Agents.Core.Models;
+
+namespace Microsoft.Agents.A365.Observability.Hosting.Middleware
+{
+ ///
+ /// Bot Framework middleware that propagates OpenTelemetry baggage context
+ /// derived from .
+ ///
+ ///
+ /// Async replies (ContinueConversation events) are passed through without
+ /// baggage setup because their context is established by the originating turn.
+ ///
+ public sealed class BaggageTurnMiddleware : IMiddleware
+ {
+ ///
+ public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default)
+ {
+ var activity = turnContext.Activity;
+ bool isAsyncReply = activity != null
+ && activity.Type == ActivityTypes.Event
+ && activity.Name == ActivityEventNames.ContinueConversation;
+
+ if (isAsyncReply)
+ {
+ await next(cancellationToken).ConfigureAwait(false);
+ return;
+ }
+
+ var builder = new BaggageBuilder();
+ builder.FromTurnContext(turnContext);
+
+ using (builder.Build())
+ {
+ await next(cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs b/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
new file mode 100644
index 00000000..f60756d3
--- /dev/null
+++ b/src/Observability/Hosting/Middleware/OutputLoggingMiddleware.cs
@@ -0,0 +1,206 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.A365.Observability.Hosting.Extensions;
+using Microsoft.Agents.A365.Observability.Runtime.Tracing.Contracts;
+using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes;
+using Microsoft.Agents.Builder;
+using Microsoft.Agents.Core.Models;
+
+namespace Microsoft.Agents.A365.Observability.Hosting.Middleware
+{
+ ///
+ /// Bot Framework middleware that creates spans
+ /// for outgoing messages.
+ ///
+ ///
+ ///
+ /// Links to a parent span when is set in
+ /// .
+ ///
+ ///
+ /// Privacy note: Outgoing message content is captured verbatim as
+ /// span attributes and exported to the configured telemetry backend.
+ ///
+ ///
+ public sealed class OutputLoggingMiddleware : IMiddleware
+ {
+ ///
+ /// The key used to store the parent
+ /// span reference. Set this value to a W3C traceparent string
+ /// (e.g. "00-{trace_id}-{span_id}-{trace_flags}") to link
+ /// spans as children of an
+ /// .
+ ///
+ public const string A365ParentSpanKey = "A365ParentSpanId";
+
+ ///
+ public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default)
+ {
+ var agentDetails = DeriveAgentDetails(turnContext);
+ var tenantDetails = DeriveTenantDetails(turnContext);
+
+ if (agentDetails == null || tenantDetails == null)
+ {
+ await next(cancellationToken).ConfigureAwait(false);
+ return;
+ }
+
+ var callerDetails = DeriveCallerDetails(turnContext);
+ var conversationId = turnContext.Activity?.Conversation?.Id;
+ var sourceMetadata = DeriveSourceMetadata(turnContext);
+ var executionType = DeriveExecutionType(turnContext);
+
+ turnContext.OnSendActivities(CreateSendHandler(
+ turnContext,
+ agentDetails,
+ tenantDetails,
+ callerDetails,
+ conversationId,
+ sourceMetadata,
+ executionType));
+
+ await next(cancellationToken).ConfigureAwait(false);
+ }
+
+ private static AgentDetails? DeriveAgentDetails(ITurnContext turnContext)
+ {
+ var recipient = turnContext.Activity?.Recipient;
+ if (recipient == null)
+ {
+ return null;
+ }
+
+ // Gate on the recipient having an agentic identity
+ var agentId = recipient.AgenticAppId ?? recipient.Id;
+ if (string.IsNullOrEmpty(agentId))
+ {
+ return null;
+ }
+
+ return new AgentDetails(
+ agentId: agentId,
+ agentName: recipient.Name,
+ agentAUID: recipient.AadObjectId,
+ agentUPN: recipient.AgenticUserId,
+ agentDescription: recipient.Role,
+ tenantId: recipient.TenantId);
+ }
+
+ private static TenantDetails? DeriveTenantDetails(ITurnContext turnContext)
+ {
+ var tenantId = turnContext.Activity?.Recipient?.TenantId;
+ if (string.IsNullOrWhiteSpace(tenantId) || !Guid.TryParse(tenantId, out var tenantGuid))
+ {
+ return null;
+ }
+
+ return new TenantDetails(tenantGuid);
+ }
+
+ private static CallerDetails? DeriveCallerDetails(ITurnContext turnContext)
+ {
+ var from = turnContext.Activity?.From;
+ if (from == null)
+ {
+ return null;
+ }
+
+ return new CallerDetails(
+ callerId: from.Id ?? string.Empty,
+ callerName: from.Name ?? string.Empty,
+ callerUpn: from.AgenticUserId ?? string.Empty,
+ tenantId: from.TenantId);
+ }
+
+ private static SourceMetadata? DeriveSourceMetadata(ITurnContext turnContext)
+ {
+ var channelId = turnContext.Activity?.ChannelId;
+ if (channelId == null)
+ {
+ return null;
+ }
+
+ return new SourceMetadata(
+ name: channelId.Channel,
+ description: channelId.SubChannel);
+ }
+
+ private static string? DeriveExecutionType(ITurnContext turnContext)
+ {
+ var pairs = turnContext.GetExecutionTypePair();
+ foreach (var pair in pairs)
+ {
+ if (pair.Key == OpenTelemetryConstants.GenAiExecutionTypeKey)
+ {
+ return pair.Value?.ToString();
+ }
+ }
+
+ return null;
+ }
+
+ private static SendActivitiesHandler CreateSendHandler(
+ ITurnContext turnContext,
+ AgentDetails agentDetails,
+ TenantDetails tenantDetails,
+ CallerDetails? callerDetails,
+ string? conversationId,
+ SourceMetadata? sourceMetadata,
+ string? executionType)
+ {
+ return async (ctx, activities, nextSend) =>
+ {
+ var messages = new List();
+ foreach (var a in activities)
+ {
+ if (string.Equals(a.Type, ActivityTypes.Message, StringComparison.OrdinalIgnoreCase)
+ && !string.IsNullOrEmpty(a.Text))
+ {
+ messages.Add(a.Text);
+ }
+ }
+
+ if (messages.Count == 0)
+ {
+ return await nextSend().ConfigureAwait(false);
+ }
+
+ // Read parent span lazily so the agent handler can set it during logic()
+ string? parentId = null;
+ if (turnContext.StackState.TryGetValue(A365ParentSpanKey, out var parentSpanValue) && parentSpanValue != null)
+ {
+ parentId = parentSpanValue.ToString();
+ }
+
+ var outputScope = OutputScope.Start(
+ agentDetails: agentDetails,
+ tenantDetails: tenantDetails,
+ response: new Response(messages),
+ conversationId: conversationId,
+ sourceMetadata: sourceMetadata,
+ callerDetails: callerDetails,
+ parentId: parentId);
+
+ try
+ {
+ outputScope.SetTagMaybe(OpenTelemetryConstants.GenAiExecutionTypeKey, executionType);
+ return await nextSend().ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ outputScope.RecordError(ex);
+ throw;
+ }
+ finally
+ {
+ outputScope.Dispose();
+ }
+ };
+ }
+ }
+}
diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Microsoft.Agents.A365.Observability.Hosting.Tests.csproj b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Microsoft.Agents.A365.Observability.Hosting.Tests.csproj
index 4c414d33..a8168905 100644
--- a/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Microsoft.Agents.A365.Observability.Hosting.Tests.csproj
+++ b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Microsoft.Agents.A365.Observability.Hosting.Tests.csproj
@@ -8,6 +8,7 @@
+
diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/BaggageTurnMiddlewareTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/BaggageTurnMiddlewareTests.cs
new file mode 100644
index 00000000..538ea909
--- /dev/null
+++ b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/BaggageTurnMiddlewareTests.cs
@@ -0,0 +1,143 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using FluentAssertions;
+using Microsoft.Agents.A365.Observability.Hosting.Middleware;
+using Microsoft.Agents.A365.Observability.Runtime.Tracing.Scopes;
+using Microsoft.Agents.Builder;
+using Microsoft.Agents.Core.Models;
+using Moq;
+using OpenTelemetry;
+
+namespace Microsoft.Agents.A365.Observability.Hosting.Tests.Middleware;
+
+[TestClass]
+public class BaggageTurnMiddlewareTests
+{
+ [TestMethod]
+ public async Task OnTurnAsync_SetsOpenTelemetryBaggage()
+ {
+ // Arrange
+ var middleware = new BaggageTurnMiddleware();
+ var turnContext = CreateTurnContext();
+
+ string? capturedTenantId = null;
+ string? capturedCallerId = null;
+
+ NextDelegate next = (ct) =>
+ {
+ capturedTenantId = Baggage.Current.GetBaggage(OpenTelemetryConstants.TenantIdKey);
+ capturedCallerId = Baggage.Current.GetBaggage(OpenTelemetryConstants.GenAiCallerIdKey);
+ return Task.CompletedTask;
+ };
+
+ // Act
+ await middleware.OnTurnAsync(turnContext, next);
+
+ // Assert
+ capturedTenantId.Should().Be("tenant-123");
+ capturedCallerId.Should().Be("caller-id");
+ }
+
+ [TestMethod]
+ public async Task OnTurnAsync_SkipsBaggageForContinueConversation()
+ {
+ // Arrange
+ var middleware = new BaggageTurnMiddleware();
+ var turnContext = CreateTurnContext(
+ activityType: ActivityTypes.Event,
+ activityName: ActivityEventNames.ContinueConversation);
+
+ bool logicCalled = false;
+ string? capturedCallerId = null;
+
+ NextDelegate next = (ct) =>
+ {
+ logicCalled = true;
+ capturedCallerId = Baggage.Current.GetBaggage(OpenTelemetryConstants.GenAiCallerIdKey);
+ return Task.CompletedTask;
+ };
+
+ // Act
+ await middleware.OnTurnAsync(turnContext, next);
+
+ // Assert
+ logicCalled.Should().BeTrue();
+ // Baggage should NOT be set because the middleware skipped it
+ capturedCallerId.Should().BeNull();
+ }
+
+ [TestMethod]
+ public async Task OnTurnAsync_CallsNextDelegate()
+ {
+ // Arrange
+ var middleware = new BaggageTurnMiddleware();
+ var turnContext = CreateTurnContext();
+
+ bool nextCalled = false;
+ NextDelegate next = (ct) =>
+ {
+ nextCalled = true;
+ return Task.CompletedTask;
+ };
+
+ // Act
+ await middleware.OnTurnAsync(turnContext, next);
+
+ // Assert
+ nextCalled.Should().BeTrue();
+ }
+
+ [TestMethod]
+ public async Task OnTurnAsync_RestoresBaggageAfterNext()
+ {
+ // Arrange
+ var middleware = new BaggageTurnMiddleware();
+ var turnContext = CreateTurnContext();
+
+ string? baggageBeforeMiddleware = Baggage.Current.GetBaggage(OpenTelemetryConstants.TenantIdKey);
+
+ NextDelegate next = (ct) => Task.CompletedTask;
+
+ // Act
+ await middleware.OnTurnAsync(turnContext, next);
+
+ // Assert – the baggage scope should be disposed after OnTurnAsync returns
+ string? baggageAfterMiddleware = Baggage.Current.GetBaggage(OpenTelemetryConstants.TenantIdKey);
+ baggageAfterMiddleware.Should().Be(baggageBeforeMiddleware);
+ }
+
+ private static ITurnContext CreateTurnContext(
+ string activityType = "message",
+ string? activityName = null)
+ {
+ var mockActivity = new Mock();
+ mockActivity.Setup(a => a.Type).Returns(activityType);
+ if (activityName != null)
+ {
+ mockActivity.Setup(a => a.Name).Returns(activityName);
+ }
+ mockActivity.Setup(a => a.Text).Returns("Hello");
+ mockActivity.Setup(a => a.From).Returns(new ChannelAccount
+ {
+ Id = "caller-id",
+ Name = "Caller",
+ AadObjectId = "caller-aad",
+ });
+ mockActivity.Setup(a => a.Recipient).Returns(new ChannelAccount
+ {
+ Id = "agent-id",
+ Name = "Agent",
+ TenantId = "tenant-123",
+ Role = "user",
+ });
+ mockActivity.Setup(a => a.Conversation).Returns(new ConversationAccount { Id = "conv-id" });
+ mockActivity.Setup(a => a.ServiceUrl).Returns("https://example.com");
+ mockActivity.Setup(a => a.ChannelId).Returns(new ChannelId("test-channel"));
+
+ var mockTurnContext = new Mock();
+ mockTurnContext.Setup(tc => tc.Activity).Returns(mockActivity.Object);
+
+ return mockTurnContext.Object;
+ }
+}
diff --git a/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/OutputLoggingMiddlewareTests.cs b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/OutputLoggingMiddlewareTests.cs
new file mode 100644
index 00000000..e714917c
--- /dev/null
+++ b/src/Tests/Microsoft.Agents.A365.Observability.Hosting.Tests/Middleware/OutputLoggingMiddlewareTests.cs
@@ -0,0 +1,146 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using FluentAssertions;
+using Microsoft.Agents.A365.Observability.Hosting.Middleware;
+using Microsoft.Agents.Builder;
+using Microsoft.Agents.Core.Models;
+using Moq;
+
+namespace Microsoft.Agents.A365.Observability.Hosting.Tests.Middleware;
+
+[TestClass]
+public class OutputLoggingMiddlewareTests
+{
+ [TestMethod]
+ public async Task OnTurnAsync_CallsNextDelegate()
+ {
+ // Arrange
+ var middleware = new OutputLoggingMiddleware();
+ var turnContext = CreateTurnContext();
+
+ bool nextCalled = false;
+ NextDelegate next = (ct) =>
+ {
+ nextCalled = true;
+ return Task.CompletedTask;
+ };
+
+ // Act
+ await middleware.OnTurnAsync(turnContext, next);
+
+ // Assert
+ nextCalled.Should().BeTrue();
+ }
+
+ [TestMethod]
+ public async Task OnTurnAsync_RegistersSendHandler_WhenRecipientHasDetails()
+ {
+ // Arrange
+ var middleware = new OutputLoggingMiddleware();
+ var mockTurnContext = new Mock();
+ SetupTurnContext(mockTurnContext);
+
+ NextDelegate next = (ct) => Task.CompletedTask;
+
+ // Act
+ await middleware.OnTurnAsync(mockTurnContext.Object, next);
+
+ // Assert
+ mockTurnContext.Verify(tc => tc.OnSendActivities(It.IsAny()), Times.Once);
+ }
+
+ [TestMethod]
+ public async Task OnTurnAsync_PassesThrough_WhenRecipientIsNull()
+ {
+ // Arrange
+ var middleware = new OutputLoggingMiddleware();
+ var mockActivity = new Mock();
+ mockActivity.Setup(a => a.Recipient).Returns((ChannelAccount)null!);
+ mockActivity.Setup(a => a.Type).Returns("message");
+
+ var mockTurnContext = new Mock();
+ mockTurnContext.Setup(tc => tc.Activity).Returns(mockActivity.Object);
+
+ bool nextCalled = false;
+ NextDelegate next = (ct) =>
+ {
+ nextCalled = true;
+ return Task.CompletedTask;
+ };
+
+ // Act
+ await middleware.OnTurnAsync(mockTurnContext.Object, next);
+
+ // Assert
+ nextCalled.Should().BeTrue();
+ mockTurnContext.Verify(tc => tc.OnSendActivities(It.IsAny()), Times.Never);
+ }
+
+ [TestMethod]
+ public async Task OnTurnAsync_PassesThrough_WhenTenantIdIsMissing()
+ {
+ // Arrange
+ var middleware = new OutputLoggingMiddleware();
+ var mockActivity = new Mock();
+ mockActivity.Setup(a => a.Type).Returns("message");
+ mockActivity.Setup(a => a.Recipient).Returns(new ChannelAccount
+ {
+ Id = "agent-id",
+ Name = "Agent",
+ // No TenantId set
+ });
+
+ var mockTurnContext = new Mock();
+ mockTurnContext.Setup(tc => tc.Activity).Returns(mockActivity.Object);
+
+ bool nextCalled = false;
+ NextDelegate next = (ct) =>
+ {
+ nextCalled = true;
+ return Task.CompletedTask;
+ };
+
+ // Act
+ await middleware.OnTurnAsync(mockTurnContext.Object, next);
+
+ // Assert
+ nextCalled.Should().BeTrue();
+ mockTurnContext.Verify(tc => tc.OnSendActivities(It.IsAny()), Times.Never);
+ }
+
+ private static ITurnContext CreateTurnContext()
+ {
+ var mockTurnContext = new Mock();
+ SetupTurnContext(mockTurnContext);
+ return mockTurnContext.Object;
+ }
+
+ private static void SetupTurnContext(Mock mockTurnContext)
+ {
+ var mockActivity = new Mock();
+ mockActivity.Setup(a => a.Type).Returns("message");
+ mockActivity.Setup(a => a.Text).Returns("Hello");
+ mockActivity.Setup(a => a.From).Returns(new ChannelAccount
+ {
+ Id = "caller-id",
+ Name = "Caller",
+ AadObjectId = "caller-aad",
+ });
+ mockActivity.Setup(a => a.Recipient).Returns(new ChannelAccount
+ {
+ Id = "agent-id",
+ Name = "Agent",
+ TenantId = "badf1f56-284d-4dc5-ac59-0dd53900e743",
+ Role = "agenticAppInstance",
+ });
+ mockActivity.Setup(a => a.Conversation).Returns(new ConversationAccount { Id = "conv-id" });
+ mockActivity.Setup(a => a.ServiceUrl).Returns("https://example.com");
+ mockActivity.Setup(a => a.ChannelId).Returns(new ChannelId("test-channel"));
+
+ mockTurnContext.Setup(tc => tc.Activity).Returns(mockActivity.Object);
+ mockTurnContext.Setup(tc => tc.StackState).Returns(new TurnContextStateCollection());
+ mockTurnContext.Setup(tc => tc.OnSendActivities(It.IsAny()))
+ .Returns(mockTurnContext.Object);
+ }
+}