Skip to content

.NET: Resilient long-running (crash-recoverable) Foundry hosted agents - #7370

Draft
rogerbarreto wants to merge 18 commits into
microsoft:mainfrom
rogerbarreto:features/foundry-hosting-resilient-agents
Draft

.NET: Resilient long-running (crash-recoverable) Foundry hosted agents#7370
rogerbarreto wants to merge 18 commits into
microsoft:mainfrom
rogerbarreto:features/foundry-hosting-resilient-agents

Conversation

@rogerbarreto

Copy link
Copy Markdown
Member

Motivation & Context

The Foundry Hosted Agents platform can keep a background response running with no client
connected and re-invoke the handler after a container crash or graceful shutdown. This adds
opt-in support for that in the .NET Microsoft.Agents.AI.Foundry.Hosting package, mirroring the
Python reference, so a workflow hosted as an AIAgent can run as a durable, crash-recoverable
Foundry Hosted Agent. Everything is off by default: a host that does not opt in behaves exactly as
before.

Description & Review Guide

  • What are the major changes?

    • Opt-in resilience surface: AddFoundryResponses(o => o.ResilientBackground = true) flows to the
      Azure SDK options; single-registration guard; agent-registration readiness check.
    • Workflow crash-recovery bridge in AgentFrameworkResponseHandler: on IsRecovery, seed the
      stream from PersistedResponse and skip re-injecting input; persist the session at each
      completed output item so a restart resumes from the last step. The mid-stream save is
      best-effort so a transient serialize race cannot abort the turn.
    • Stable executor ids for resilient workflows: a readiness health check fails fast when a workflow
      step's agent has an auto-generated id (which would break checkpoint resume), backed by the
      internal AIAgent.HasAutoGeneratedId signal and WorkflowHostAgent.GetService<Workflow>().
    • Two focused samples (resilient workflow, steering), ADR 0032, and deterministic recovery tests.
  • What is the impact of these changes?
    Additive and off by default. Existing hosted agents are unaffected unless they opt in.

Related Issue

Fixes #

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 is linked to an issue and there is no other open PR for this issue.
  • This is not a breaking change.

Point the AgentServer package versions at the drop that carries the resilient /
checkpointing / steering surface (Core beta.27, Invocations beta.6, Responses
beta.7) and add the agentserver-preview-local source mapped to
Azure.AI.AgentServer.*.
Document, in plain language, when durable long-running hosting applies
(background requests only; workflow or safe-to-redo single agent), the recovery
contract, the resilience gate, host-wide single registration, the
agent-registration readiness health check, session-store durability, and the
two focused samples.
Lean opt-in wiring and the crash-recovery save cadence, all inert unless
resilience is turned on:

- AddFoundryResponses flows Action<ResponsesServerOptions> straight to the SDK
  (no wrapper options type).
- Single-registration guard: the Responses server is host-wide (one per
  process). A prior registration is detected via the ResponseHandler it always
  registers; the single-agent AddFoundryResponses(agent) extension throws an
  actionable message if combined or repeated, and the parameterless overload is
  a no-op on repeat.
- Agent-registration readiness health check: unhealthy until at least one
  AIAgent is registered (keyed or default), scanning service descriptors so it
  never constructs an agent.
- Resilience gate: the handler reads IOptions<ResponsesServerOptions> for
  ResilientBackground; ShouldPersistForResilience requires background and store.
- Save cadence: on a resilient turn (or IsRecovery), persist the session at each
  completed output item so a mid-turn crash leaves the last workflow step on
  disk; on graceful shutdown, ExitForRecoveryAsync instead of emitting
  incomplete.
- Tests for the guard and the health check.
- Hosted-Workflow-Resilient: the translation-chain workflow hosted with
  ResilientBackground enabled, so a background response survives a container
  crash and resumes from the workflow's last completed step. README has a local
  crash-and-recover walkthrough.
- Hosted-Steering: a chat agent hosted with SteerableConversations enabled, so a
  follow-up input for an in-progress conversation is queued instead of rejected.

Both opt in with a single option (o => o.ResilientBackground/SteerableConversations
= true), are off by default, registered in agent-framework-dotnet.slnx, build
clean, and answer GET /readiness with 200 Healthy locally.
On a crash-recovery re-invocation (context.IsRecovery), align the handler with
the SDK's resilient pattern so a resumed background response reaches a terminal
state instead of stalling:

- Seed the event stream from the last durable snapshot (context.PersistedResponse)
  instead of the request, so the resumed stream continues from the output items
  already emitted before the crash. The output-index allocator advances to
  PersistedResponse.Output.Count and re-emitted items do not collide with the
  already-durable slots.
- Skip re-injecting the original input on recovery. The persisted session already
  carries the progress (a workflow hosted as an agent resumes from its last
  checkpoint, restored by GetSessionAsync), so re-injecting would enqueue a
  duplicate turn on top of the resumed state and the run would never terminate.

Both changes are gated on IsRecovery, so a normal (non-recovery) turn is
unchanged.
A workflow checkpoint records each step by its executor id, and an agent-backed
step derives that id from the agent's Id (and Name). By default an agent gets a
fresh random Id per process, so after a crash the restarted process would rebuild
the workflow with different ids and the saved checkpoint would no longer match,
failing the resume with "checkpoint is not compatible with the workflow".

Give each translation agent a fixed Id and Name so the rebuilt workflow is
identical across restarts and the checkpoint resumes cleanly. A code comment
spells out why this matters for resilient workflows.
Add the minimal, non-public hooks the Foundry resilient-hosting readiness check
needs to detect, ahead of time, a workflow whose agent-backed steps would not
survive a crash-recovery resume:

- AIAgent.HasAutoGeneratedId (internal): true when no explicit id was supplied
  (IdCore is null), so Id falls back to a per-process random value. This is the
  authoritative signal: a caller-supplied id that happens to be a GUID is
  correctly reported as stable. Exposed to the hosting package via a new
  InternalsVisibleTo entry on the abstractions assembly.
- WorkflowHostAgent.GetService now returns the hosted Workflow for an unkeyed
  Workflow service request, so a host can inspect the workflow's executor ids
  (and their backing agents) without constructing a session. Additive, with no
  change to existing behaviour.

WorkflowHostAgentTests covers the GetService enabler.
A workflow hosted as an agent resumes after a crash by matching its checkpoint
to the rebuilt workflow by executor id. An agent-backed step derives that id from
the agent's id, so an agent with an auto-generated id produces a different id on
every start and the resume fails deep in the engine with "the specified
checkpoint is not compatible with the workflow".

Add a /readiness health check that, only while ResilientBackground is on,
inspects every registered workflow-host agent (via GetService<Workflow>) and
reports Unhealthy with an actionable message when any agent-backed step has an
auto-generated id (AIAgent.HasAutoGeneratedId). This turns a late, cryptic resume
failure into an early not-ready signal. It reads already-constructed agent
instances from the binding raw value, so a caller-supplied GUID id is correctly
treated as stable, and it never creates a session. On a non-resilient host it is
a no-op.

Because the check reads abstractions internals via InternalsVisibleTo, the
hosting assembly now consumes the shared Throw and DiagnosticIds sources from the
abstractions assembly instead of injecting its own copies, matching how other
projects consume a referenced assembly's shared sources.
Use options: new() to set each agent's stable Id and Name (the named argument
picks the options overload so the target-typed new() is unambiguous), and add a
README note explaining why resilient workflows need stable executor ids.
Record in the .NET AGENTS.md that a test file is named after its system under
test (the type tested), and that a single file should not mix multiple systems
under test.
The non-recovery history-replay guard treated a session as a resume when its
state bag held any entry. A hosted session is stamped once, on its first turn,
with a write-once HostedSessionContext, which makes the state bag non-empty from
the first turn, so counting entries could make a brand new conversation look like
a resume and skip loading the history of a conversation the platform resumed by
id.

Decide instead from a specific marker captured at the right time: whether the
session already carried a HostedSessionContext before this turn stamped one. A
session the store loaded from a prior turn has it; a freshly created session does
not. This reflects whether a prior turn established the session without relying on
how many state-bag entries happen to exist. In local (non-hosted) use no context
is stamped, so a fresh turn loads history as before.
State the requirement directly: for resilient workflows every workflow step agent
must have an explicit id for crash-recovery resume, and list the step agents to
fix.
Final pass so the ADR reflects what shipped: the recovery bridge seeds from
PersistedResponse and skips input re-injection; progress is persisted per output
item through the existing FileSystemAgentSessionStore rather than a separate
resilient store type; and resilient workflows require stable executor ids, backed
by the internal AIAgent.HasAutoGeneratedId signal, the WorkflowHostAgent Workflow
service, and a second readiness health check. Implementation-plan items done in
this work are marked done and the Python-to-.NET mapping is corrected.
A workflow hosted as an agent advances on its own thread, so serializing the
session at each completed output item (the resilient checkpoint cadence) can race
the running workflow's in-memory checkpoint writes and throw "Collection was
modified". Because that save sat in the streaming loop's finally-only try, the
exception escaped the handler, the orchestrator logged "Handler failed", and the
response was left stuck in_progress forever. On a crash-recovery re-invocation the
resumed workflow drains its buffered supersteps in a burst, so the race was
effectively guaranteed and recovery never completed.

Wrap the incremental save in a try/catch that logs and continues. It is a pure
durability optimization: the serialize throws before any file is written, so a
failed attempt leaves the previously persisted session intact, and the
authoritative save in the finally block (after the stream drains and the workflow
is quiescent) persists the final consistent state. A transient snapshot race must
never abort the turn.

Verified with a local crash-and-recover cycle: a run crashed mid-progress now
resumes and reaches status=completed with no "Handler failed" in the restart,
where before it stayed in_progress.
Drive the handler with a fake agent that records the messages it receives and a
fake session store, so the crash-recovery semantics can be asserted without a
real model, a real process crash, or timing:

- On a recovery re-invocation the handler injects no input (the restored session
  drives the resume), while a fresh turn feeds the request input to the agent.
- A mid-stream session save that throws (the serialize race) is swallowed and the
  turn still reaches a completed terminal event instead of escaping as a handler
  failure.
Copilot AI review requested due to automatic review settings July 28, 2026 18:44
@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 workflows Usage: [Issues, PRs], Target: Workflows labels Jul 28, 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.

Pull request overview

Adds opt-in support in the .NET Foundry hosting layer for resilient long-running background Responses (crash recovery) and steerable conversations, aligning behavior with the existing Python reference while keeping the default behavior unchanged unless explicitly enabled.

Changes:

  • Plumbs ResponsesServerOptions configuration through AddFoundryResponses(...) and adds single-registration guarding + readiness health checks (agent registration + stable workflow executor IDs when resilience is enabled).
  • Updates AgentFrameworkResponseHandler to support recovery semantics (IsRecovery + PersistedResponse) and incremental best-effort session persistence at output-item boundaries.
  • Adds ADR 0032 plus focused samples and unit tests covering the new resilience/readiness behaviors.

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
dotnet/tests/Microsoft.Agents.AI.Workflows.UnitTests/WorkflowHostAgentTests.cs Verifies WorkflowHostAgent.GetService<Workflow>() exposes the hosted workflow and returns null for plain agents.
dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ServiceCollectionExtensionsTests.cs Adds tests for server single-registration behavior and readiness health check registration.
dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ResilientWorkflowExecutorIdHealthCheckTests.cs Covers readiness failure/success cases for resilient workflow executor ID stability detection.
dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentRegistrationHealthCheckTests.cs Validates readiness behavior when no agent is registered and descriptor-based detection for keyed/default agents.
dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentFrameworkResponseHandlerResilienceTests.cs Deterministic tests for recovery input handling, persisted-response seeding, and mid-stream save failure tolerance.
dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowHostAgent.cs Extends GetService to return hosted Workflow for inspection without constructing a session.
dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs Adds ResponsesServerOptions configuration callback, enforces single server registration, and registers readiness health checks.
dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ResilientWorkflowExecutorIdHealthCheck.cs Implements readiness health check validating stable executor IDs for resilient workflow hosting.
dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj Adjusts packaging/build properties for the hosting project.
dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AgentRegistrationHealthCheck.cs Adds readiness health check to fail fast when no agent is registered.
dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AgentFrameworkResponseHandler.cs Implements recovery-aware stream seeding, recovery input handling changes, and incremental checkpoint persistence for resilience.
dotnet/src/Microsoft.Agents.AI.Abstractions/Microsoft.Agents.AI.Abstractions.csproj Exposes internals to Foundry.Hosting to enable readiness checks without expanding public API.
dotnet/src/Microsoft.Agents.AI.Abstractions/AIAgent.cs Adds internal HasAutoGeneratedId signal to detect unstable agent IDs.
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Resilient/README.md Documents resilient background responses and crash-recovery workflow behavior with local walkthrough.
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Resilient/Program.cs Sample showing a resilient hosted workflow with explicit stable agent IDs.
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Resilient/HostedWorkflowResilient.csproj Sample project wiring for resilient workflow hosting.
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Resilient/Dockerfile.contributor Contributor container build path for ProjectReference-based sample.
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Resilient/Dockerfile End-user Dockerfile for the resilient workflow sample.
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Resilient/agent.yaml Sample hosted agent definition (container protocol).
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Resilient/agent.manifest.yaml Sample manifest for initializing/deploying the resilient workflow template.
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Resilient/.env.example Sample environment configuration for local runs.
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Steering/README.md Documents steerable conversations behavior and usage with Responses protocol.
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Steering/Program.cs Sample showing steerable conversations enabled via ResponsesServerOptions.
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Steering/HostedSteering.csproj Sample project wiring for steerable hosting scenario.
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Steering/Dockerfile.contributor Contributor container build path for ProjectReference-based sample.
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Steering/Dockerfile End-user Dockerfile for the steering sample.
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Steering/agent.yaml Sample hosted agent definition (container protocol).
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Steering/agent.manifest.yaml Sample manifest for initializing/deploying the steering template.
dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Steering/.env.example Sample environment configuration for local runs.
dotnet/nuget.config Adds an additional package source and source mapping configuration.
dotnet/Directory.Packages.props Updates Azure.AI.AgentServer.* package versions to betas carrying the resilience surface.
dotnet/AGENTS.md Codifies test-file naming conventions (test file named after SUT; keep tests for a type together).
dotnet/agent-framework-dotnet.slnx Adds the new samples to the solution.
docs/decisions/0032-foundry-hosting-resilient-long-running-agents.md ADR documenting the resilience/steering design, recovery contract, and readiness safeguards.

Comment thread dotnet/nuget.config
Comment thread dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AgentFrameworkResponseHandler.cs Outdated

@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.

Automated Code Review

Reviewers: 5 | Confidence: 89%

✓ Correctness

The PR adds opt-in resilient long-running agent support. The logic is sound and well-tested. The primary issue is a development artifact in nuget.config: a Windows-specific local package source path (C:\local_packages) that will cause warnings or failures in CI and on non-Windows machines. A secondary concern is passing a potentially-cancelled cancellation token to ExitForRecoveryAsync during shutdown.

✓ Security Reliability

The most critical issue is a local developer package source (C:\local_packages) committed to nuget.config, which is a supply-chain risk on shared machines and will cause restore warnings/failures on CI (Linux). The resilience implementation itself is well-structured with appropriate gating and error handling. The mid-stream save's catch-all for non-cancellation exceptions is reasonable given the documented serialize-race scenario. The ExitForRecoveryAsync call during shutdown uses the original cancellation token which could already be in a cancelling state, though this is a lower-confidence concern.

✓ Test Coverage

The PR adds good focused tests for resilience health checks, recovery semantics, and registration guard logic. However, there are notable test coverage gaps: the core ExitForRecoveryAsync shutdown-deferral path is untested, the foundry-resilient-workflow-ids health check registration is not verified end-to-end (unlike the foundry-agent-registration check which has a test), and there is no test verifying that non-resilient turns (Background=false or Store=false) correctly skip incremental persistence. Additionally, the nuget.config commits a hardcoded Windows-local package source that will break CI/restore on non-Windows environments.

✓ Failure Modes

The PR introduces well-tested crash-recovery semantics with properly gated, best-effort mid-stream saves and a clear recovery contract. The primary operational concern is a committed development artifact in nuget.config that adds a Windows-specific local package source (C:\local_packages) which will not exist in CI or for other contributors, potentially blocking builds if the referenced Azure.AI.AgentServer beta versions are not yet on nuget.org.

✓ Design Approach

I found two design gaps in the new resilience registration path. The resilient-workflow readiness check only inspects prebuilt agent instances, so it silently misses workflow agents registered through keyed DI factories even though that registration style is already used elsewhere in this repo; that defeats the PR's stated goal of surfacing unstable executor IDs before traffic is routed. Separately, repeated parameterless AddFoundryResponses calls silently discard any later ResponsesServerOptions configuration, which can leave resilience or steering disabled even when the caller explicitly opts in on the second call.

Suggestions

  • Add a test verifying that when shutdown is detected on a resilient turn, ExitForRecoveryAsync is called and no terminal event is emitted (the core crash-recovery deferral path at AgentFrameworkResponseHandler.cs:490-494 is currently untested).

Automated review by rogerbarreto's agents

Comment thread dotnet/nuget.config
Comment thread dotnet/nuget.config
Comment thread dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs Outdated
…authoritative resume detection

GetSessionAsync was a hidden get-or-create that always returned a usable session, so the handler could not tell a resumed conversation from a fresh one and inferred resume from HostedSessionContext, which is only stamped on hosted turns. That regressed local (non-hosted) resumes, replaying history over restored state. GetSessionAsync now returns null when nothing is stored (a plain lookup, never creating), which is the authoritative resume signal in both local and hosted runs and for workflows and chat agents. A new virtual GetOrCreateSessionAsync keeps the convenience path. The handler now derives isResume from whether the store actually loaded a session.
…covers factory registrations

The readiness health check read AIAgent registrations straight from the service descriptors, which only exposes agents registered as a concrete instance. Agents registered via a factory (including keyed factory registrations, the documented way to host multiple workflow agents) were never inspected, so their executor ids went unchecked. Resolving from the service provider instead covers both instance and factory registrations. Keys are discovered from the descriptors because a service provider cannot enumerate keyed registrations on its own; agents are singletons so each is constructed at most once and cached.
…ns beta.7, Responses beta.8

Refreshed local preview drop from the long-running-agents-private-preview main branch. Foundry.Hosting builds clean and the full unit suite stays green (345/345) on the new surface.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs .NET Usage: [Issues, PRs], Target: .Net workflows Usage: [Issues, PRs], Target: Workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants