Skip to content

.NET: [BREAKING] Support scoped and transient agent lifetimes in Foundry Hosting - #10238

Draft
Roger Barreto (rogerbarreto) wants to merge 4 commits into
microsoft:mainfrom
rogerbarreto:foundry-agent-di-lifetimes
Draft

Roger Barreto (rogerbarreto) wants to merge 4 commits into
microsoft:mainfrom
rogerbarreto:foundry-agent-di-lifetimes

Conversation

@rogerbarreto

@rogerbarreto Roger Barreto (rogerbarreto) commented Nov 10, 2026 •

Copy link
Copy Markdown
Member

Motivation & Context

During review of #10132, westey (@westey-m) noted that Foundry hosting needs more options for configuring the agent DI scope (comment). This PR follows up on that.

Microsoft.Agents.AI.Hosting already lets callers choose a ServiceLifetime for agents and for session stores (#4476, issue #4457), and the generic OpenAI Responses host resolves agents from a scope that stays open for the whole streamed response (#8423). Foundry hosting had neither:

  • AgentFrameworkResponseHandler is a singleton and resolved every agent and session store from the root IServiceProvider. It never created a scope.
  • A Scoped agent therefore threw on every request when scope validation was on. When validation was off, it silently became one shared instance for the whole process.
  • A Transient agent that implements IDisposable was kept alive by the root container until shutdown.
  • Both readiness health checks also resolved agents from the root container, so a Scoped registration failed there too.
  • A session store could only be passed as an object together with an agent, and that store also became the fallback store for every other agent.

The HTTP request scope cannot be reused for this. Background responses keep running after the HTTP response is sent, and a resilient recovery calls the handler again in a new process with no HTTP request at all.

Description & Review Guide

  • What are the major changes?
    • AgentFrameworkResponseHandler.CreateAsync creates one AsyncServiceScope per call and resolves the agent and the session store from it. await using disposes the scope when the event stream ends, whether the run completes, fails, is cancelled, the client disconnects, or the run is handed off for recovery. This is the same pattern as HostedAgentResponseExecutor in the OpenAI host.
    • The hosted user-identity check now runs before any agent or store is created, so a rejected request never invokes a caller-supplied factory.
    • HostedStoredOutputHealthCheck and HostedWorkflowCheckpointingHealthCheck resolve agents inside a short-lived scope.
    • New AddFoundryResponses overloads on both IHostApplicationBuilder and IServiceCollection take an agent factory and a ServiceLifetime (Singleton by default, like AddAIAgent in Microsoft.Agents.AI.Hosting):
      // Existing, unchanged: one agent object for the process
      builder.AddFoundryResponses(myAgent);
      
      // New
      builder.AddFoundryResponses(
          "my-agent",
          sp => new MyAgent(sp.GetRequiredService<MyDbContext>()),
          ServiceLifetime.Scoped);
      • The agent is registered only under the name. There is no non-keyed registration that forwards to the keyed one, because Microsoft DI records a factory's result for disposal once per registration, and a forwarding registration would dispose the same object twice.
      • When no default agent exists yet, an internal FoundryDefaultAgentName records this agent's name, and the response handler routes requests without a matching agent name to it. The instance overload no longer takes the default slot when a factory agent already holds it.
      • An existing keyed registration takes precedence, matching the instance overload's TryAdd behavior. The factory call then adds nothing for the agent.
    • New AddFoundryAgentSessionStore overloads on both builders register a session store separately from agents, following the precedent of WithSessionStore in Microsoft.Agents.AI.Hosting. The store's lifetime is independent of the agent's, which is safe because the handler uses the store directly and passes the agent on every call:
      // For every agent without a store of its own. DI never disposes an object you pass in.
      builder.AddFoundryAgentSessionStore(mySessionStore);
      
      // A factory with its own lifetime (Singleton by default), optionally for one agent only.
      builder.AddFoundryAgentSessionStore(
          sp => new MySessionStore(sp.GetRequiredService<MyDbContext>()),
          ServiceLifetime.Scoped,
          agentName: "my-agent");
      • For each response, the handler uses the store registered under the agent's name, then the store registered for all agents, then the default store. A request answered by the default agent uses that agent's own store.
      • Calling it again for the same agent name, or again for all agents, replaces the earlier registration, including one made by AddFoundryResponses(agent, store) or directly in DI, in whatever order the methods are called.
    • Breaking behavior: AddFoundryResponses(agent, store) now registers store only under the agent's name. On main it also became the store for all agents. Without a store, it no longer registers a separate default store under the agent's name, so the agent falls back to the store registered for all agents.
    • Factory agents store sessions where the instance overload would put an agent with the same name registered in the same order: name:{agentName} for the agent that receives requests without a matching name, and key:{agentName} for every other agent. That holds for every lifetime. The handler checks at run time whether a non-keyed AIAgent took the default role, so such an agent never shares sessions with a factory agent of the same name.
    • Falling back from an agent's own store to the store registered for all agents is now the normal path, so that log message moves from Warning to Debug.
    • XML docs explain each lifetime, how a store is chosen, that moving an agent to a different store leaves its earlier sessions behind, and that the response scope is not the ASP.NET Core request scope. The Hosted-ChatClientAgent README gains an "Agent lifetime" section.
  • What is the impact of these changes?
    • Singleton registrations behave exactly as before, because a scope returns the same root instance.

    • Scoped and Transient registrations now work, including keyed registrations made by hand and AddAIAgent(..., lifetime) from Microsoft.Agents.AI.Hosting.

    • Lifetime behavior, for agents and for stores registered with a factory:

      Lifetime Created Disposed
      Singleton once per process once, at app shutdown
      Scoped once per response run once, when that run's event stream ends, even after the HTTP request has returned
      Transient on every lookup once, when that run's event stream ends
    • Conversation state still comes from the saved session, not from the agent object. A new agent per turn therefore loses nothing.

    • Breaking behavior, only for apps that mix registration styles:

      1. AddFoundryResponses(agentA, storeA) plus another agent registered by hand: the other agent moves from storeA to the store registered for all agents, or the default store.
      2. A non-keyed AgentSessionStore registered by hand plus AddFoundryResponses(agentA) without a store: agent A moves from its own default store to the store registered by hand.

      In both cases the affected agent's earlier sessions stay in the old store and are no longer found. No error is raised; those conversations start without their history. Apps with a single agent, apps that add several agents with AddFoundryResponses(agent, ...), and apps that only use AddFoundryResponses() with agents registered by hand are not affected. No public API from main is removed or changed.

  • What do you want reviewers to focus on?
    • The shape of the new public APIs: a separate AddFoundryAgentSessionStore with its own lifetime, replace semantics where the last call wins, and a store factory that does not receive the agent name.
    • The breaking store selection change for apps that mix registration styles.
    • Default routing for factory agents through FoundryDefaultAgentName instead of a forwarding DI registration, and the storage identity rule in ResolveAgent.
    • Whether the scope boundary (one scope per CreateAsync enumeration) is the right owner for steering and recovery.

Related Issue

Follow-up to deferred review feedback on #10132: #10132 (comment). Related to #4457 and #4476. No open PR covers this.

AI Assistance

  • No material AI assistance was used.
  • This is an AI-assisted contribution. I reviewed, understood, and verified all submitted content and accept responsibility for it.

AI assistance details: investigation of the existing DI flow, implementation, tests, documentation, a security review of the new registration and resolution paths, and this PR description.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR links to an agreed issue with no competing open PR, or the Related Issue section documents a trivial-change or repository-automation exception.
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

Resolve agents and session stores from a DI scope owned by each response run, so scoped and transient registrations are created per run and disposed when the event stream ends, including background and recovered runs. Health checks resolve agents from a short lived scope.

Add AddFoundryResponses overloads that take an agent factory, a ServiceLifetime, and an optional session store factory sharing that lifetime. Factory registrations keep the name based session storage identity for every lifetime.
Copilot AI balanced review requested due to automatic review settings November 10, 2026 15:46
@agent-framework-automation agent-framework-automation Bot added documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs .NET Usage: [Issues, PRs], Target: .Net labels Nov 10, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Default aliases can resolve scoped agents from the root provider and dispose instances twice, while factory migration changes non-default storage identities, making existing saved sessions inaccessible.

3 open issues
What changed in this PR

Adds configurable agent lifetimes to .NET Foundry Hosting, extending existing hosting support so scoped and transient services can remain alive for an entire response run, including background execution and recovery.

Changes:

  • Resolve agents and session stores within response-owned scopes, and use short-lived scopes for readiness checks.
  • Add factory-based registration overloads with configurable lifetimes and storage-identity tracking.
  • Document lifetime behavior and add tests for disposal, background execution, and recovery.
File Description
dotnet/​tests/​Microsoft.Agents.AI.Foundry.Hosting.UnitTests/​ResilientTwoLifetimeIntegrationTests.cs Extends recovery tests to scoped and transient factories.
dotnet/​tests/​Microsoft.Agents.AI.Foundry.Hosting.UnitTests/​HostedWorkflowCheckpointingHealthCheckTests.cs Tests scoped workflow resolution during readiness checks.
dotnet/​tests/​Microsoft.Agents.AI.Foundry.Hosting.UnitTests/​HostedStoredOutputHealthCheckTests.cs Tests scoped agent resolution during stored-output checks.
dotnet/​tests/​Microsoft.Agents.AI.Foundry.Hosting.UnitTests/​FoundryAgentLifetimeTests.cs Adds registration, disposal, identity, and background-lifetime tests.
dotnet/​src/​Microsoft.Agents.AI.Foundry.Hosting/​ServiceCollectionExtensions.cs Adds factory registration with configurable service lifetimes.
dotnet/​src/​Microsoft.Agents.AI.Foundry.Hosting/​HostedWorkflowCheckpointingHealthCheck.cs Resolves workflow agents within an asynchronous scope.
dotnet/​src/​Microsoft.Agents.AI.Foundry.Hosting/​HostedStoredOutputHealthCheck.cs Scopes agent resolution and stored-output probing.
dotnet/​src/​Microsoft.Agents.AI.Foundry.Hosting/​HostApplicationBuilderExtensions.cs Exposes factory registration through the host builder.
dotnet/​src/​Microsoft.Agents.AI.Foundry.Hosting/​FoundryAgentFactoryRegistration.cs Tracks factory registrations and session-storage identities.
dotnet/​src/​Microsoft.Agents.AI.Foundry.Hosting/​AgentFrameworkResponseHandler.cs Owns response-run scopes and applies factory storage identities.
dotnet/​samples/​04-hosting/​FoundryHostedAgents/​responses/​Hosted-ChatClientAgent/​README.md Documents agent lifetimes and factory-based registration.

Review effort: Balanced

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAF Automated Review — Iteration 1

Result: Findings reported
Scope: full PR (1 commit(s)): 3f6d66df7af0
Model: gpt-5.6-sol

Overview

The PR correctly moves response and readiness resolution into async scopes, and its tests establish scope survival through background execution, cancellation, recovery, and normal disposal. The keyed/default forwarding design also preserves one scoped instance and stable session identity when both registrations are created together. Residual risk remains when the overload composes with pre-existing keyed or default registrations, and the forwarding descriptor gives DI two disposal owners for one instance.

Reviewed the supplied pull-request change set across correctness, security/reliability, architecture, and failure behavior.
4 verified findings remained after source verification (4 medium) across 1 file. Details are attached to the affected lines below.

Affected areas: dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs

Register factory agents and stores only under their name so DI disposes each object once and no forwarding registration can mismatch lifetimes. Route requests without an agent name to the factory default in the response handler.

Use the instance overload storage identities: name based for the effective default, key based otherwise. Decide the effective default from the built container so a non keyed agent registered later never shares sessions with a factory agent.

Validate the hosted user identity before creating any agent or store, and document that the response scope is not the HTTP request scope.
Add AddFoundryAgentSessionStore on IServiceCollection and IHostApplicationBuilder.
It registers a session store object, or a factory with its own DI lifetime
(Singleton by default), for one agent name or for every agent without a store
of its own. A later call for the same target replaces the earlier one, in any
order relative to AddFoundryResponses.

The agent factory overload no longer takes a store factory, so a scoped agent
no longer forces its store to be scoped too. A store passed to
AddFoundryResponses(agent, store) is now used only by that agent, and agents
without a store fall back to the store registered for all agents. The default
agent looks up the store registered under its own name.

Replace the factory registration markers with a single FoundryDefaultAgentName.
The handler now checks at run time whether a non-keyed agent took the default
role, so the IServiceProviderIsService lookup is no longer needed.
Explain in the XML docs and the sample README that a store passed to AddFoundryResponses(agent, store) serves only that agent, that agents without a store use the store registered for all agents, and that moving an agent to a different store leaves its earlier sessions in the old store.
@rogerbarreto Roger Barreto (rogerbarreto) changed the title .NET: Support scoped and transient agent lifetimes in Foundry Hosting .NET: [BREAKING] Support scoped and transient agent lifetimes in Foundry Hosting Nov 10, 2026
@agent-framework-automation agent-framework-automation Bot added the breaking change Usage: [PRs], Target: all PRs that introduce changes that are not backward compatible label Nov 10, 2026
@rogerbarreto

Copy link
Copy Markdown
Member Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAF Automated Review — Iteration 2

Result: Findings reported
Scope: 3 net-new commit(s): 2587519e3f2c, 94f66ee5e13a, 8e316de27039
Model: gpt-5.6-sol

Overview

The change gives each response run an asynchronously disposed DI scope and avoids duplicate ownership of factory-created agents, with focused tests covering scope lifetime, exact-once disposal, session identity, and health-check resolution. Hosted identity validation also occurs before caller factories run. One routing-order defect remains: a request that explicitly selects a keyed agent can still be blocked by construction of an unrelated non-keyed default.

Reviewed the supplied incremental change set across correctness, security/reliability, architecture, and failure behavior.
1 verified finding remained after source verification (1 medium) across 1 file. Details are attached to the affected lines below.

Affected areas: dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AgentFrameworkResponseHandler.cs

private (AIAgent Agent, string? SessionStoreKey) ResolveAgent(CreateResponse request, IServiceProvider services)
{
var agentName = GetAgentName(request);
var defaultAgent = services.GetService<AIAgent>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolving the non-keyed default before checking the requested key means every named request constructs that unrelated agent too. If its transient/scoped factory throws or performs expensive side effects, a healthy keyed agent can no longer serve its own requests. Resolve the keyed target first and avoid instantiating the non-keyed default unless it is actually selected, while preserving storage identity through registration metadata.

This branch was successfully deployed

1 active deployment
github-app-auth — 8e316de2 Deployed Nov 10, 2026 by rogerbarreto via add_label #27545
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Usage: [PRs], Target: all PRs that introduce changes that are not backward compatible documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs .NET Usage: [Issues, PRs], Target: .Net

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants