Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
>
Expand All @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ public override async IAsyncEnumerable<ResponseStreamEvent> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ public static class AgentSessionExtensions
/// <summary>
/// Gets the trusted Foundry user id associated with a hosted agent session.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="session">The session to read from.</param>
/// <returns>The platform user id, or <see langword="null"/> when no hosted user identity was resolved.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="session"/> is <see langword="null"/>.</exception>
public static string? GetHostedUserId(this AgentSession session)
public static string? GetFoundryHostedUserId(this AgentSession session)
{
Throw.IfNull(session);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,22 @@ namespace Microsoft.Agents.AI.Foundry.Hosting;
public sealed class FoundryResponsesServerOptions : ResponsesServerOptions
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// <para>
/// 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 <c>store</c> setting. Disabling inference-service storage
/// does not disable Foundry response storage.
/// </para>
/// <para>
/// While this is <see langword="false"/>, 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 <see langword="true"/> 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 <see langword="false"/>, hosting sets <c>store</c> to <see langword="false"/> 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 <see langword="true"/>
/// to leave the agent's own storage setting unchanged and skip that check.
/// </para>
/// <para>
/// A local conversation marker used by the framework to persist chat history in the agent
Expand All @@ -62,16 +62,27 @@ public sealed class FoundryResponsesServerOptions : ResponsesServerOptions
public string OAuthConsentInterruptedToolResult { get; set; } = McpConsentResults.NotExecuted;

/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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 <see langword="true"/> to omit them. It applies only while
/// <see cref="InferenceStoredOutputAllowed"/> is <see langword="false"/>, because that is when hosting
/// turns storage off and the reasoning items would otherwise be lost between turns.
/// <para>
/// While <see cref="InferenceStoredOutputAllowed"/> is <see langword="false"/>, hosting adds
/// <c>reasoning.encrypted_content</c> 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.
/// </para>
/// <para>
/// Set this option to <see langword="true"/> 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 <see cref="InferenceStoredOutputAllowed"/> is <see langword="true"/>.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
/// <value>
/// Default is <see langword="false"/>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ static async Task<AIAgent> 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.");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Foundry.Hosting.IntegrationTests.TestContainer;
/// Minimal agent that echoes the platform user isolation key as <c>USER-ID:&lt;key&gt;</c>.
/// Does not call a model, so identity ITs do not depend on OpenAI quota or catalog access.
/// </summary>
#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";
Expand Down Expand Up @@ -63,7 +63,7 @@ protected override ValueTask<AgentSession> 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}";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArgumentNullException>(() => session.GetFoundryHostedUserId());
}

[Fact]
public void SetHostedUserId_NullOrWhitespace_Throws()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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(
Expand Down
Loading