From 6d1c4928847ac15aaba9e41f8258ae2b55cda28a Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Mon, 9 Nov 2026 20:06:24 -0800 Subject: [PATCH] fix(foundry)!: clarify identity and storage APIs Rename GetHostedUserId to GetFoundryHostedUserId to identify the Foundry-specific contract. Preserve the serialized session key. Clarify inference storage and encrypted reasoning XML documentation. Leave agent lifetime configuration for a separate change. BREAKING CHANGE: replace GetHostedUserId() calls with GetFoundryHostedUserId(). Refs #10132 --- ...hosted-platform-context-agentserver-2.0.md | 4 +- .../responses/Hosted-MemoryAgent/Program.cs | 4 +- .../AgentFrameworkResponseHandler.cs | 2 +- .../AgentSessionExtensions.cs | 7 ++- .../FoundryResponsesServerOptions.cs | 49 ++++++++++++------- .../Program.cs | 2 +- .../UserIdentityEchoAgent.cs | 4 +- .../AgentFrameworkResponseHandlerTests.cs | 2 +- .../AgentSessionExtensionsTests.cs | 32 ++++++++++-- .../HostedUserIdTests.cs | 10 ++-- 10 files changed, 78 insertions(+), 38 deletions(-) diff --git a/docs/decisions/0034-hosted-platform-context-agentserver-2.0.md b/docs/decisions/0034-hosted-platform-context-agentserver-2.0.md index 06703c2b096..673f106adc3 100644 --- a/docs/decisions/0034-hosted-platform-context-agentserver-2.0.md +++ b/docs/decisions/0034-hosted-platform-context-agentserver-2.0.md @@ -12,7 +12,7 @@ informed: [] Supersedes [ADR-0026](0026-hosted-session-identity-context.md). > **Update (2026-11-05):** `HostedFoundryMemoryProviderScopes` was removed during pre-stable -> API cleanup. Hosted memory samples now read `session.GetHostedUserId()` directly when +> API cleanup. Hosted memory samples now read `session.GetFoundryHostedUserId()` directly when > constructing `FoundryMemoryProvider.State`, keeping the trusted identity-to-memory-scope mapping > visible without a memory-specific hosting helper. > @@ -22,7 +22,7 @@ Supersedes [ADR-0026](0026-hosted-session-identity-context.md). > AgentServer to populate `PlatformContext`. > > The public `HostedSessionContext` / `GetHostedContext()` surface was replaced by -> `AgentSession.GetHostedUserId()`. The hosting layer persists the resolved user id as a plain string +> `AgentSession.GetFoundryHostedUserId()`. The hosting layer persists the resolved user id as a plain string > in the session state bag, making clear that it is a user partition and not the Foundry hosted > session id. diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Program.cs index 1d3de0e2a57..abdd8df59f8 100644 --- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Program.cs +++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/Program.cs @@ -9,7 +9,7 @@ // Memory scope flows from request -> hosting layer -> session -> provider: // 1. Foundry sets x-agent-user-id on every inbound request. // 2. The hosting layer reads context.PlatformContext.UserIdKey and associates it with the session. -// 3. FoundryMemoryProvider's stateInitializer reads that id via GetHostedUserId and uses it as the +// 3. FoundryMemoryProvider's stateInitializer reads that id via GetFoundryHostedUserId and uses it as the // FoundryMemoryProviderScope, partitioning memories per user. using Azure.AI.Projects; @@ -99,7 +99,7 @@ static FoundryMemoryProvider.State CreatePerUserMemoryState(AgentSession? sessio // Repository builds use the reduced session API; published 1.27 exposes the context object. string userId = #if USE_LOCAL_AGENT_FRAMEWORK_SOURCE - session?.GetHostedUserId() + session?.GetFoundryHostedUserId() #else session?.GetHostedContext()?.UserId #endif diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AgentFrameworkResponseHandler.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AgentFrameworkResponseHandler.cs index 715d0819091..04ce0df7d2d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AgentFrameworkResponseHandler.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AgentFrameworkResponseHandler.cs @@ -176,7 +176,7 @@ public override async IAsyncEnumerable CreateAsync( // no user identity, so there is nothing to partition or tamper-check and the session is shared. if (session is not null && resolvedUserId is not null) { - string? existingUserId = session.GetHostedUserId(); + string? existingUserId = session.GetFoundryHostedUserId(); if (existingUserId is null) { // Fresh path: the session has no hosted context yet (either freshly created here, diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AgentSessionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AgentSessionExtensions.cs index 46323d30346..2e2442d1e13 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AgentSessionExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AgentSessionExtensions.cs @@ -15,10 +15,15 @@ public static class AgentSessionExtensions /// /// Gets the trusted Foundry user id associated with a hosted agent session. /// + /// + /// Reads the identity saved in the session, not the current request context. Foundry Responses + /// hosting assigns this value when it resolves a user identity. Other hosting integrations do + /// not currently assign it. + /// /// The session to read from. /// The platform user id, or when no hosted user identity was resolved. /// Thrown when is . - public static string? GetHostedUserId(this AgentSession session) + public static string? GetFoundryHostedUserId(this AgentSession session) { Throw.IfNull(session); diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryResponsesServerOptions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryResponsesServerOptions.cs index f088e38c6f7..b2b0f86a2c9 100644 --- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryResponsesServerOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryResponsesServerOptions.cs @@ -22,22 +22,22 @@ namespace Microsoft.Agents.AI.Foundry.Hosting; public sealed class FoundryResponsesServerOptions : ResponsesServerOptions { /// - /// Gets or sets a value indicating whether the agent's own chat client is allowed to store the - /// responses it produces. + /// Gets or sets a value indicating whether the agent's chat client is allowed to store responses + /// in the inference service it calls. /// /// /// - /// A hosted turn is already recorded by the storage provider that runs around this handler, and - /// that record is the conversation the caller reads back. When the service behind the agent's chat - /// client also stores the turn, the same exchange is written a second time onto a trail of its own, - /// which nothing here reads and no one reconciles with the first. + /// This option controls storage in the inference service, not storage of the hosted response + /// returned to the caller. Foundry response storage is configured separately through the hosting + /// Responses API, including the caller's store setting. Disabling inference-service storage + /// does not disable Foundry response storage. /// /// - /// While this is , hosting turns that storage off for every run (the "store" - /// property in the JSON representation), and the readiness probe reports an agent whose - /// configuration would keep it on. Set it to to leave the agent's own - /// setting exactly as the container configured it, in which case hosting neither changes it nor - /// checks it. + /// While this is , hosting sets store to on + /// OpenAI Responses and Chat Completions requests sent by the agent's chat client to the inference + /// service. Other request types are left unchanged. The readiness probe reports an agent whose + /// configuration would keep inference-service storage on. Set this option to + /// to leave the agent's own storage setting unchanged and skip that check. /// /// /// A local conversation marker used by the framework to persist chat history in the agent @@ -62,16 +62,27 @@ public sealed class FoundryResponsesServerOptions : ResponsesServerOptions public string OAuthConsentInterruptedToolResult { get; set; } = McpConsentResults.NotExecuted; /// - /// Gets or sets a value indicating whether to exclude an encrypted version of reasoning tokens from - /// reasoning item outputs. + /// Gets or sets a value indicating whether hosting should refrain from requesting encrypted + /// reasoning content from the inference service. /// /// - /// By default, encrypted reasoning tokens are included so reasoning items can be used in multi-turn - /// conversations when using the Responses API statelessly (like when the store parameter is set to - /// false, or when an organization is enrolled in the zero data retention program). Set this option - /// to to omit them. It applies only while - /// is , because that is when hosting - /// turns storage off and the reasoning items would otherwise be lost between turns. + /// + /// While is , hosting adds + /// reasoning.encrypted_content to the included properties of OpenAI Responses requests sent + /// by the agent's chat client. Encrypted reasoning content is an opaque value that compatible + /// clients can send back to the inference service to continue reasoning across turns without + /// storing the response in that service. + /// + /// + /// Set this option to to prevent hosting from adding that included property. + /// It does not remove an included property already configured by the agent. This option has no + /// effect while is . + /// + /// + /// This option changes requests to the inference service, not the hosted response returned to the + /// caller or Foundry response storage. Requesting encrypted reasoning content does not by itself + /// ensure that hosting preserves it or sends it back on later turns. + /// /// /// /// Default is . diff --git a/dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer/Program.cs b/dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer/Program.cs index bc0879c8edb..1e31a665853 100644 --- a/dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer/Program.cs +++ b/dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer/Program.cs @@ -343,7 +343,7 @@ static async Task CreateMemoryAgentAsync(AIProjectClient client, string static FoundryMemoryProvider.State CreatePerUserMemoryState(AgentSession? session) { string userId = - session?.GetHostedUserId() + session?.GetFoundryHostedUserId() ?? throw new InvalidOperationException( "Foundry hosted user identity is required to scope memories."); diff --git a/dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer/UserIdentityEchoAgent.cs b/dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer/UserIdentityEchoAgent.cs index a0116efbd74..beb800ee4c6 100644 --- a/dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer/UserIdentityEchoAgent.cs +++ b/dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer/UserIdentityEchoAgent.cs @@ -12,7 +12,7 @@ namespace Foundry.Hosting.IntegrationTests.TestContainer; /// Minimal agent that echoes the platform user isolation key as USER-ID:<key>. /// Does not call a model, so identity ITs do not depend on OpenAI quota or catalog access. /// -#pragma warning disable MAAI001 // GetHostedUserId / experimental surface +#pragma warning disable MAAI001 // GetFoundryHostedUserId / experimental surface internal sealed class UserIdentityEchoAgent : AIAgent { public override string Name => "user-identity-agent"; @@ -63,7 +63,7 @@ protected override ValueTask DeserializeSessionCoreAsync( private static string BuildReply(AgentSession? session) { - var userId = session?.GetHostedUserId(); + var userId = session?.GetFoundryHostedUserId(); var token = string.IsNullOrWhiteSpace(userId) ? "USER-ID:missing" : $"USER-ID:{userId}"; return $"ready\n{token}"; } diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentFrameworkResponseHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentFrameworkResponseHandlerTests.cs index 42a1035fa38..58b4d06d557 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentFrameworkResponseHandlerTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentFrameworkResponseHandlerTests.cs @@ -1860,7 +1860,7 @@ public async Task CreateAsync_PreviousResponseIdChain_NoConversation_ReusesOneSe // Assert: both turns persisted under the same partition key → one created session. Assert.NotNull(agent.LastSession); - Assert.Equal("alice", agent.LastSession!.GetHostedUserId()); + Assert.Equal("alice", agent.LastSession!.GetFoundryHostedUserId()); Assert.Equal(1, agent.SessionCount); } diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentSessionExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentSessionExtensionsTests.cs index 8788b4cdde2..7c76880811a 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentSessionExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentSessionExtensionsTests.cs @@ -7,32 +7,56 @@ namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests; public sealed class AgentSessionExtensionsTests { [Fact] - public void GetHostedUserId_ReturnsNullWhenAbsent() + public void GetFoundryHostedUserId_ReturnsNullWhenAbsent() { // Arrange var session = new TestAgentSession(); // Act - var userId = session.GetHostedUserId(); + var userId = session.GetFoundryHostedUserId(); // Assert Assert.Null(userId); } [Fact] - public void SetHostedUserId_ThenGetHostedUserId_RoundTrips() + public void SetHostedUserId_ThenGetFoundryHostedUserId_RoundTrips() { // Arrange var session = new TestAgentSession(); // Act session.SetHostedUserId("alice"); - var userId = session.GetHostedUserId(); + var userId = session.GetFoundryHostedUserId(); // Assert Assert.Equal("alice", userId); } + [Fact] + public void GetFoundryHostedUserId_ReadsExistingStateKey() + { + // Arrange + var session = new TestAgentSession(); + session.StateBag.SetValue("Microsoft.Agents.AI.Foundry.Hosting.HostedUserId", "alice"); + + // Act + var userId = session.GetFoundryHostedUserId(); + + // Assert + Assert.Equal("alice", userId); + } + + [Fact] + public void GetFoundryHostedUserId_NullSession_Throws() + { + // Arrange + AgentSession session = null!; + + // Act & Assert + Assert.Throws(() => session.GetFoundryHostedUserId()); + } + [Fact] public void SetHostedUserId_NullOrWhitespace_Throws() { diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostedUserIdTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostedUserIdTests.cs index eb329ad533d..63afa477ad8 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostedUserIdTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostedUserIdTests.cs @@ -82,7 +82,7 @@ public async Task AspNetMiddleware_OverridesUserIdBeforeAgentHandlerAsync() response.StatusCode == HttpStatusCode.OK, $"Expected HTTP 200 but received {(int)response.StatusCode}: {body}"); Assert.NotNull(capturingAgent.LastSession); - Assert.Equal(TestUserId, capturingAgent.LastSession.GetHostedUserId()); + Assert.Equal(TestUserId, capturingAgent.LastSession.GetFoundryHostedUserId()); } [Fact] @@ -99,7 +99,7 @@ public async Task Handler_FreshSession_AppliesUserIdentityFromPlatformContextAsy // Assert Assert.NotNull(capturingAgent.LastSession); - Assert.Equal(TestUserId, capturingAgent.LastSession.GetHostedUserId()); + Assert.Equal(TestUserId, capturingAgent.LastSession.GetFoundryHostedUserId()); } [Fact] @@ -118,7 +118,7 @@ public async Task Handler_NullKeysFromProvider_NotHosted_SucceedsWithoutContextA // Assert: no throw, a session was produced, and it carries no hosted identity context. Assert.NotNull(capturingAgent.LastSession); - Assert.Null(capturingAgent.LastSession.GetHostedUserId()); + Assert.Null(capturingAgent.LastSession.GetFoundryHostedUserId()); } [Fact] @@ -154,7 +154,7 @@ await sessionStore.SaveSessionAsync( // Assert Assert.NotNull(capturingAgent.LastSession); - Assert.Equal(TestUserId, capturingAgent.LastSession.GetHostedUserId()); + Assert.Equal(TestUserId, capturingAgent.LastSession.GetFoundryHostedUserId()); } [Fact] @@ -217,7 +217,7 @@ await sessionStore.SaveSessionAsync( // Assert Assert.NotNull(capturingAgent.LastSession); - Assert.Equal("alice", capturingAgent.LastSession.GetHostedUserId()); + Assert.Equal("alice", capturingAgent.LastSession.GetFoundryHostedUserId()); } private static AgentFrameworkResponseHandler BuildHandler(