Skip to content

Commit 0d53d11

Browse files
authored
.NET: [BREAKING] Bump Azure.AI.AgentServer to 2.0.0 protocol and migrate Foundry.Hosting (microsoft#6800)
* .NET: Bump Azure.AI.AgentServer to 2.0.0 protocol and migrate Foundry.Hosting Bumps Core .25->.26, Invocations .4->.5, Responses .5->.6 and adopts the 2.0.0 container protocol. Breaking change: IsolationContext (UserIsolationKey + ChatIsolationKey) is replaced by PlatformContext (UserIdKey from x-agent-user-id, CallId from x-agent-foundry-call-id). The per-chat key is gone; HostedSessionContext is now user-only and the per-request CallId is forwarded outbound to Foundry first-party services (toolbox/MCP). Also fixes a real call-id egress bug: AsyncLocal writes inside the streaming response iterator are reverted across yield boundaries, so the call id was dropped before the toolbox/MCP egress ran. The handler now re-applies HostedCallContext.CallId before each egress point. Adds HostedConversationKey to map a request to a stable MAF AgentSession via conversation_id, else the partition key embedded in previous_response_id, else the minted response id. This keeps store=false previous_response_id chains and conversation_id forks on a single hosted MAF session without using the container session id. Sample manifests bump the responses protocol to 2.0.0 (invocations stays 1.0.0). Integration tests split store/session semantics into HostedResponsesStoreConfigTests with its own scenario, read stored responses through the per-agent endpoint client, and inject the model deployment into the container. * Pin Azure.Core 1.59.0 for Hosted-Workflow-Handoff sample AgentServer 1.0.0-beta.26 (pulled transitively via Foundry.Hosting) requires Azure.Core 1.59.0. This sample disables transitive pinning and references Azure.Core directly, so override just this project to the SDK-required version without moving the solution-wide central pin. * Add guard test for request-scoped call-id cleanup Asserts HostedCallContext.CallId does not leak into the caller's execution context after CreateAsync's stream completes, while confirming the agent run still observed the call id. Documents the request-scoped contract and guards against stale-header leakage across requests handled on the same thread. * Refresh hosting READMEs for AgentServer 2.0 migration Updates stale docs to match the shipped code: the MemoryAgent README now describes the x-agent-user-id user-identity header (chat isolation key removed) feeding HostedSessionContext.UserId; the IntegrationTests README corrects the scenario count (six to eleven), adds the missing memory scenario row, and stops claiming all scenarios are skipped now that several are validated and active. * Add ADR 0030 superseding 0026 for AgentServer 2.0 platform context Documents the migration from ResponseContext.Isolation (UserIsolationKey/ChatIsolationKey) to ResponseContext.PlatformContext (UserIdKey/CallId): user-only HostedSessionContext, the request-scoped HostedCallContext call-id forwarded on egress, HostedConversationKey session keying, and removal of the PerChat/PerUserAndChat memory scopes. Marks ADR 0026 as superseded. * Add breaking-change v2.0-only disclaimer to package metadata Augments the package Description and adds PackageReleaseNotes stating this release targets the Foundry Responses container protocol v2.0 only, is not compatible with v1, and directs consumers to a previous release for the v1 protocol definition. * Address review comments: dead chat-key surface and weak test assertions Fixes the automated review findings: the MemoryAgent/AgentSkills .env.example now say one variable (only HOSTED_USER_ISOLATION_KEY remains); the MemoryAgent smoke script drops the unused ChatKey parameter and its call-site arguments; HostedConversationKey null test now exercises a real null (and whitespace); and the reuse-one-session test asserts an exact SessionCount of 1 instead of <= 1.
1 parent 8721068 commit 0d53d11

68 files changed

Lines changed: 917 additions & 531 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/decisions/0026-hosted-session-identity-context.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
---
2-
status: accepted
2+
status: superseded by [ADR-0030](0030-hosted-platform-context-agentserver-2.0.md)
33
contact: rogerbarreto
4-
date: 2026-05-07
4+
date: 2026-06-29
55
deciders: rogerbarreto
66
consulted: []
77
informed: []
88
---
99

1010
# Hosted session identity context for Foundry Hosting
1111

12+
> **Superseded by [ADR-0030](0030-hosted-platform-context-agentserver-2.0.md).** `Azure.AI.AgentServer.*` 2.0.0 (responses protocol `2.0.0`) replaced `ResponseContext.Isolation` (`UserIsolationKey` / `ChatIsolationKey`, headers `x-agent-user-isolation-key` / `x-agent-chat-isolation-key`) with `ResponseContext.PlatformContext` (`UserIdKey` / `CallId`, headers `x-agent-user-id` / `x-agent-foundry-call-id`). The chat isolation key was removed and `HostedSessionContext` is now user-only. This ADR is retained as the historical record of the original design.
13+
1214
## Context and Problem Statement
1315

1416
Server-hosted Foundry agents need a way to scope per-user state (most notably `FoundryMemoryProvider` memories) by the end user that initiated the request. The Foundry platform already injects `x-agent-user-isolation-key` and `x-agent-chat-isolation-key` headers on every Responses request, but the agent-framework hosting layer did not surface those values to `AIContextProvider` instances. The provider's `stateInitializer` only received an `AgentSession?` with no identity attached, so per-user scoping was impossible without out-of-band plumbing.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
---
2+
status: accepted
3+
contact: rogerbarreto
4+
date: 2026-06-29
5+
deciders: rogerbarreto
6+
consulted: []
7+
informed: []
8+
---
9+
10+
# Hosted platform context (user id + call id) for Foundry Hosting on AgentServer 2.0
11+
12+
Supersedes [ADR-0026](0026-hosted-session-identity-context.md).
13+
14+
## Context and Problem Statement
15+
16+
[ADR-0026](0026-hosted-session-identity-context.md) sourced the hosted-agent end-user identity from `ResponseContext.Isolation` (an `IsolationContext` typed `UserIsolationKey` / `ChatIsolationKey`), injected by the platform as the `x-agent-user-isolation-key` and `x-agent-chat-isolation-key` headers.
17+
18+
`Azure.AI.AgentServer.*` 2.0.0 (responses protocol `2.0.0`) removes that surface. `ResponseContext.Isolation` is gone; the platform now exposes `ResponseContext.PlatformContext` (a `PlatformContext` typed `UserIdKey` and `CallId`), populated from the `x-agent-user-id` and `x-agent-foundry-call-id` headers. The chat isolation key no longer exists, and a new per-request **call id** is introduced that first-party Foundry services (the toolbox proxy in particular) require on outbound calls to resolve the server-side-stored caller context. The hosting layer in `Microsoft.Agents.AI.Foundry.Hosting` had to migrate to this contract without changing the public shape that samples and providers depend on.
19+
20+
## Decision Drivers
21+
22+
- Track the breaking `Azure.AI.AgentServer.*` 2.0.0 surface (`PlatformContext` replacing `Isolation`) while keeping the same per-user partitioning guarantees from ADR-0026.
23+
- Keep the change **internal**: existing hosted samples and `AIContextProvider`s must not need code changes. `session.GetHostedContext().UserId`, `HostedSessionIsolationKeyProvider`, and `AddFoundryResponses` stay source-compatible.
24+
- Forward the new per-request call id verbatim on outbound calls to Foundry first-party services so per-user toolbox OAuth consent and other server-side caller-context lookups keep working.
25+
- Remain resilient on protocol `1.0.0`: when only the legacy headers are present, `UserIdKey` still resolves and `CallId` is simply absent.
26+
- Preserve the strict-resume tamper defense from ADR-0026 with identity now reduced to user only.
27+
28+
## Considered Options
29+
30+
For the identity source:
31+
32+
1. **Map `ResponseContext.PlatformContext.UserIdKey`** into the existing `HostedSessionContext` (user only), keeping ADR-0026's storage shape and read accessor.
33+
2. Keep a `ChatId` slot on `HostedSessionContext` for backward source-compatibility, populated from `CallId` or left null.
34+
35+
For the call id propagation:
36+
37+
A. **A request-scoped ambient (`HostedCallContext`, an `AsyncLocal<string?>`)** set by the handler and re-applied before each egress point, read by the outbound delegating handler.
38+
B. Thread the call id through every method signature down to the toolbox bearer handler.
39+
40+
For session keying (previously implied by the conversation/chat pairing):
41+
42+
I. **`HostedConversationKey`** resolving a stable partition from `conversation_id ?? partition(previous_response_id) ?? partition(responseId)`.
43+
II. Continue keying on the container session id (`FOUNDRY_AGENT_SESSION_ID`).
44+
45+
## Decision Outcome
46+
47+
Chosen: **Option 1** for identity, **Option A** for call id, **Option I** for session keying.
48+
49+
Rationale:
50+
51+
- **`ChatId` dropped (Option 2 rejected).** The platform no longer supplies a chat key; carrying a synthetic one would invent identity the trust boundary does not provide. `HostedSessionContext` becomes user-only (`HostedSessionContext(string userId)` / `UserId`), and the strict-resume check validates `UserId` alone. The corresponding `HostedFoundryMemoryProviderScopes` values `PerChat` and `PerUserAndChat` are removed; `PerUser` is retained.
52+
- **Ambient call id (Option B rejected).** Writing `HostedCallContext.CallId` inside the streaming `async IAsyncEnumerable` iterator is reverted across each `yield`, so a single up-front assignment is lost before the toolbox/MCP egress runs. The handler therefore captures `context.PlatformContext?.CallId` once and **re-applies it immediately before each egress point**; `FoundryToolboxBearerTokenHandler` forwards it as `x-agent-foundry-call-id`. The ambient is request-scoped and never leaks into the caller's execution context (guarded by a unit test).
53+
- **`HostedConversationKey` (Option II rejected).** One container serves many conversations for its lifetime, so the container session id cannot key per-conversation state. The partition key is derived from the conversation/`previous_response_id`/minted response id instead.
54+
55+
Implementation summary in `Microsoft.Agents.AI.Foundry.Hosting`:
56+
57+
| Type | Visibility | Change vs ADR-0026 |
58+
|---|---|---|
59+
| `HostedSessionContext` | public sealed | Now user-only (`UserId`); `ChatId` removed. |
60+
| `PlatformHostedSessionIsolationKeyProvider` | internal sealed | Maps `context.PlatformContext.UserIdKey` (was `context.Isolation.UserIsolationKey` / `ChatIsolationKey`). |
61+
| `HostedCallContext` | internal static | New. Request-scoped `AsyncLocal<string?>` holding the `x-agent-foundry-call-id` value. |
62+
| `HostedConversationKey` | internal | New. Resolves the per-conversation partition key. |
63+
| `FoundryToolboxBearerTokenHandler` | internal | Now also forwards `x-agent-foundry-call-id` outbound. |
64+
| `HostedFoundryMemoryProviderScopes` | public | `PerChat` / `PerUserAndChat` removed; `PerUser` kept. |
65+
66+
Package manifests bump the responses container protocol to `2.0.0` (invocations stays `1.0.0`).
67+
68+
## Consequences
69+
70+
Positive:
71+
72+
- Per-user memory partitioning and the strict-resume tamper defense from ADR-0026 are preserved with no public API churn for samples or providers.
73+
- Per-user toolbox OAuth consent and other server-side caller-context lookups keep working because the per-request call id is forwarded on egress.
74+
- Works unchanged on protocol `1.0.0` (no call id) and `2.0.0`.
75+
76+
Negative:
77+
78+
- `HostedSessionContext.ChatId` and the `PerChat` / `PerUserAndChat` memory scopes are removed; any out-of-tree consumer that referenced them must move to user-scoped partitioning.
79+
- The call id must be re-applied before every egress point because of the async-iterator `AsyncLocal` revert; a missed re-apply silently drops the header. This is covered by unit tests.
80+
81+
## Out of scope
82+
83+
- HMAC tamper signatures over the persisted context remain unimplemented; equality comparison against `ResponseContext.PlatformContext` on every request is sufficient because the platform sets the header at the trust boundary.
84+
- The per-request `User` field on `CreateResponse` is still intentionally not consumed.

dotnet/Directory.Packages.props

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="13.0.0" />
2424
<PackageVersion Include="MessagePack" Version="3.1.7" /> <!-- Transitive dependency of Aspire pinned to newer version due to vulnerability in 2.5.192 -->
2525
<!-- Azure.* -->
26-
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.25" />
27-
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.4" />
28-
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.5" />
26+
<PackageVersion Include="Azure.AI.AgentServer.Core" Version="1.0.0-beta.26" />
27+
<PackageVersion Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.5" />
28+
<PackageVersion Include="Azure.AI.AgentServer.Responses" Version="1.0.0-beta.6" />
2929
<PackageVersion Include="Azure.Search.Documents" Version="12.0.0" />
3030
<PackageVersion Include="Azure.AI.Projects" Version="2.1.0-beta.3" />
3131
<PackageVersion Include="Azure.AI.Agents.Persistent" Version="1.2.0-beta.10" />
Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FOUNDRY_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
1+
FOUNDRY_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
22
ASPNETCORE_URLS=http://+:8088
33
ASPNETCORE_ENVIRONMENT=Development
44
FOUNDRY_MODEL=gpt-4o
@@ -8,7 +8,6 @@ SKILL_NAMES=support-style,escalation-policy
88
# In production, skills are provisioned externally — leave this unset or false.
99
PROVISION_SAMPLE_SKILLS=true
1010
AZURE_BEARER_TOKEN=DefaultAzureCredential
11-
# When running outside the Foundry platform the platform-injected isolation keys are absent.
12-
# These two variables provide fallback values for local Docker debugging only.
13-
HOSTED_USER_ISOLATION_KEY=local-dev-user
14-
HOSTED_CHAT_ISOLATION_KEY=local-dev-chat
11+
# When running outside the Foundry platform the platform-injected user-identity key is absent.
12+
# This variable provides a fallback value for local Docker debugging only.
13+
HOSTED_USER_ISOLATION_KEY=local-dev-user

dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/Dockerfile.contributor

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Dockerfile for contributors building from the agent-framework repository source.
1+
# Dockerfile for contributors building from the agent-framework repository source.
22
#
33
# This project uses ProjectReference to the local Microsoft.Agents.AI source,
44
# which means a standard multi-stage Docker build cannot resolve dependencies outside
@@ -10,7 +10,6 @@
1010
# docker run --rm -p 8088:8088 \
1111
# -e AGENT_NAME=hosted-agent-skills \
1212
# -e HOSTED_USER_ISOLATION_KEY=alice \
13-
# -e HOSTED_CHAT_ISOLATION_KEY=alice-chat-1 \
1413
# --env-file .env hosted-agent-skills
1514
#
1615
# For end-users consuming the NuGet package (not ProjectReference), use the standard
@@ -20,4 +19,4 @@ WORKDIR /app
2019
COPY out/ .
2120
EXPOSE 8088
2221
ENV ASPNETCORE_URLS=http://+:8088
23-
ENTRYPOINT ["dotnet", "HostedAgentSkills.dll"]
22+
ENTRYPOINT ["dotnet", "HostedAgentSkills.dll"]

dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.manifest.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
1+
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
22
name: hosted-agent-skills
33
displayName: "Hosted Agent Skills"
44

@@ -21,7 +21,7 @@ template:
2121
kind: hosted
2222
protocols:
2323
- protocol: responses
24-
version: 1.0.0
24+
version: 2.0.0
2525
resources:
2626
cpu: "0.25"
2727
memory: 0.5Gi

dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
1+
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
22
kind: hosted
33
name: hosted-agent-skills
44
protocols:
55
- protocol: responses
6-
version: 1.0.0
6+
version: 2.0.0
77
resources:
88
cpu: "0.25"
99
memory: 0.5Gi

dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/scripts/smoke.ps1

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#requires -Version 7
1+
#requires -Version 7
22
<#
33
.SYNOPSIS
44
Local smoke test for the Hosted-AgentSkills sample.
@@ -51,7 +51,6 @@ function Start-Container {
5151
-e AGENT_NAME=hosted-agent-skills `
5252
-e AZURE_BEARER_TOKEN=$bearer `
5353
-e HOSTED_USER_ISOLATION_KEY=smoke-user `
54-
-e HOSTED_CHAT_ISOLATION_KEY=smoke-chat-1 `
5554
--env-file .env `
5655
$ImageName | Out-Host
5756
if ($LASTEXITCODE -ne 0) { throw "docker run failed." }
@@ -97,4 +96,4 @@ try {
9796
}
9897
finally {
9998
docker rm -f $ContainerName 2>$null | Out-Null
100-
}
99+
}

dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/agent.manifest.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
1+
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
22
name: hosted-azure-search-rag
33
displayName: "Hosted Azure AI Search RAG Agent"
44

@@ -22,7 +22,7 @@ template:
2222
kind: hosted
2323
protocols:
2424
- protocol: responses
25-
version: 1.0.0
25+
version: 2.0.0
2626
resources:
2727
cpu: "0.25"
2828
memory: 0.5Gi
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
1+
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
22
kind: hosted
33
name: hosted-azure-search-rag
44
protocols:
55
- protocol: responses
6-
version: 1.0.0
6+
version: 2.0.0
77
resources:
88
cpu: "0.25"
99
memory: 0.5Gi

0 commit comments

Comments
 (0)