.NETC#AI AgentsDevOpsMicrosoft FoundryRésilience

Hébergez un workflow .NET dans Microsoft Foundry, reprenez une réponse en arrière-plan après incident et protégez les effets de bord par idempotence.

Yva Hajatiana
17 octobre 2026
13 min de lecture
Partager :X / TwitterLinkedIn
Composition éditoriale abstraite sur l’ingénierie logicielle et les opérations

Reprendre un workflow IA .NET après un redémarrage

Une réponse d’agent peut durer plus longtemps que le processus qui l’héberge : analyse d’un incident, génération d’un rapport ou attente d’une validation. Relancer le processus sans stratégie de reprise laisse alors deux mauvais choix : perdre le travail, ou rejouer aveuglément une opération qui a peut-être déjà créé un ticket ou déclenché un changement.

Microsoft Agent Framework a récemment ajouté, dans son intégration Microsoft Foundry, des réponses en arrière-plan résilientes pour les workflows .NET. Avec ResilientBackground, l’hôte associe la réponse asynchrone à des checkpoints persistants et peut reprendre le dernier point durable après un redémarrage. La fonctionnalité est récente : elle ne remplace ni l’idempotence des systèmes externes ni une autorisation métier.

Ce tutoriel construit le squelette d’un agent de compte rendu d’incident. Il publie un endpoint compatible Responses, exécute le workflow en arrière-plan, puis montre comment rendre une action externe rejouable sans doublon. Les tests et la CI vérifient les garanties déterministes ; un vrai modèle n’est nécessaire que pour le test d’intégration isolé.

Flux montrant un client qui démarre un workflow, l’enregistrement d’un checkpoint sécurisé, une interruption, la reprise par un nouvel hôte puis une action externe protégée contre les doublons
Le checkpoint permet de reprendre le workflow ; l’idempotence reste indispensable après la dernière frontière durable.

Ce qui est nouveau, et ce que cela ne résout pas

Une réponse en arrière-plan classique donne surtout un identifiant à interroger. Les réponses en arrière-plan d’un agent .NET expliquent déjà ce modèle de polling. Ici, le sujet est différent : le serveur Foundry conserve l’état nécessaire pour qu’un workflow reprenne après la disparition du processus qui le servait.

ProblèmeRéponse résiliente FoundryCe qui reste à concevoir
Le processus redémarreReprise depuis le dernier checkpoint de workflow sauvegardéNoms et identifiants d’exécuteurs stables
Le client ne doit pas rester connectéRéponse background identifiée et consultableUI de statut et timeout métier
Une étape a pu appeler un système externeLe workflow peut rejouer après la dernière sauvegardeClé d’idempotence vérifiée par le système cible
Plusieurs personnes utilisent l’agentStockage et protocole de réponse isolent les états gérésAutorisation applicative, isolation tenant et rétention

La distinction est capitale : un checkpoint est une frontière de progrès du workflow, pas une transaction distribuée. Un arrêt juste après POST /tickets et juste avant la sauvegarde suivante peut conduire à une nouvelle tentative. Chaque écriture externe doit donc porter une clé stable, par exemple workflowRunId + étape, que la cible refuse de traiter deux fois.

Prérequis

Prévoyez :

  • le SDK .NET 10 ou une version explicitement supportée par les packages Agent Framework retenus ;
  • un projet Microsoft Foundry avec un déploiement de modèle, pour le test de bout en bout ;
  • Azure CLI connectée (az login) et Azure Developer CLI si vous déployez l’exemple ;
  • PowerShell sous Windows ;
  • une identité Foundry à privilèges minimaux, distincte de toute identité de déploiement.

Les packages évoluent rapidement. Commencez avec --prerelease seulement pour découvrir une combinaison qui fonctionne, puis verrouillez les versions validées dans votre dépôt. Dans un projet réel, utilisez aussi un fichier de verrouillage NuGet et faites examiner toute mise à jour comme un changement de plateforme.

dotnet new web --framework net10.0 --name ResilientIncidentAgent
Set-Location ResilientIncidentAgent

dotnet add package Microsoft.Agents.AI
dotnet add package Microsoft.Agents.AI.Workflows
dotnet add package Microsoft.Agents.AI.Foundry.Hosting --prerelease
dotnet add package Azure.AI.Projects
dotnet add package Azure.Identity
dotnet restore --use-lock-file

Le package d’hébergement expose l’adaptateur ASP.NET Core et le protocole Responses. Conservez les paramètres d’accès hors du code et du dépôt :

$env:FOUNDRY_PROJECT_ENDPOINT = "https://<votre-projet>.services.ai.azure.com/api/projects/<projet>"
$env:FOUNDRY_MODEL_DEPLOYMENT = "<deploiement>"

En production, remplacez la chaîne d’identifiants de développement par une identité managée explicitement configurée. Le processus de l’agent ne doit recevoir aucun rôle permettant de modifier une production simplement parce qu’il peut analyser un incident.

1. Définir un workflow dont les identités ne changent pas

Un hôte qui redémarre doit reconstruire le même graphe pour retrouver un checkpoint. Donnez donc un nom stable au workflow et des identifiants stables aux exécuteurs. Ne dérivez jamais ces valeurs d’un utilisateur, d’un ticket ou d’un identifiant de requête.

Créez IncidentWorkflow.cs :

using System.Text.Json;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;

namespace ResilientIncidentAgent;

public sealed record IncidentInput(string IncidentId, string Summary);
public sealed record IncidentReport(string IncidentId, string Finding);

public sealed class ParseIncidentRequest()
    : ChatProtocolExecutor("parse-incident-request",
        new ChatProtocolExecutorOptions { AutoSendTurnToken = false })
{
    protected override ProtocolBuilder ConfigureProtocol(ProtocolBuilder builder)
        => base.ConfigureProtocol(builder).SendsMessage<IncidentInput>();

    protected override async ValueTask TakeTurnAsync(
        List<ChatMessage> messages,
        IWorkflowContext context,
        bool? emitEvents,
        CancellationToken cancellationToken = default)
    {
        string json = messages.Last(message => message.Role == ChatRole.User).Text;
        IncidentInput input = JsonSerializer.Deserialize<IncidentInput>(
            json, new JsonSerializerOptions(JsonSerializerDefaults.Web))
            ?? throw new ArgumentException("Une entrée JSON est requise.");

        ArgumentException.ThrowIfNullOrWhiteSpace(input.IncidentId);
        ArgumentException.ThrowIfNullOrWhiteSpace(input.Summary);
        await context.SendMessageAsync(input, cancellationToken: cancellationToken);
    }
}

public sealed class AnalyzeIncident()
    : Executor<IncidentInput, ChatMessage>("analyze-incident")
{
    public override ValueTask<ChatMessage> HandleAsync(
        IncidentInput input,
        IWorkflowContext context,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(input.IncidentId))
        {
            throw new ArgumentException("L’incident est requis.", nameof(input));
        }

        var report = new IncidentReport(
            input.IncidentId,
            $"Analyse déterministe prête pour {input.IncidentId} : {input.Summary}");

        return ValueTask.FromResult(new ChatMessage(ChatRole.Assistant, report.Finding));
    }
}

public static class IncidentWorkflow
{
    public static Workflow Create()
    {
        var parse = new ParseIncidentRequest();
        var analyze = new AnalyzeIncident();

        return new WorkflowBuilder(parse)
            .AddEdge(parse, analyze)
            .WithOutputFrom(analyze)
            .WithName("incident-recovery-v1")
            .Build();
    }
}

L’exécuteur d’entrée ChatProtocolExecutor accumule les messages et attend le TurnToken du protocole agent, désérialise le dernier message utilisateur JSON avec les noms camelCase et transmet un IncidentInput validé à AnalyzeIncident. Le rapport est converti en ChatMessage assistant, déclaré comme sortie du workflow pour alimenter la réponse agent.

Cet exemple ne fait pas encore appel à un modèle : la première étape reste déterministe afin de rendre le contrat exécutable en CI. Dans une application complète, ajoutez un agent ou un outil après cette étape, en conservant le même nom et les mêmes identifiants lorsque vous déployez une nouvelle instance. Les checkpoints Agent Framework indiquent qu’en .NET l’identité d’un exécuteur dépend notamment de son nom et de son identifiant ; les modifier rend les anciens checkpoints incompatibles.

2. Exposer le workflow dans Foundry avec reprise en arrière-plan

L’extension AsAIAgent() permet de présenter un workflow comme un agent. AddFoundryResponses() branche ensuite le serveur Responses. La configuration de résilience doit être effectuée lors du premier appel à AddFoundryResponses : l’Agent Server y enregistre ses tâches durables.

Remplacez Program.cs par ceci :

using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry.Hosting;
using Microsoft.Agents.AI.Workflows;
using ResilientIncidentAgent;

Workflow workflow = IncidentWorkflow.Create();

AIAgent agent = workflow.AsAIAgent(
    id: "incident-recovery-agent",
    name: "incident-recovery-agent",
    includeWorkflowOutputsInResponse: true);

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();

builder.Services.AddFoundryResponses(
    agent,
    configure: options => options.ResilientBackground = true);

var app = builder.Build();
app.MapFoundryResponses();
app.MapHealthChecks("/healthz");

await app.RunAsync();

Pour qu’une requête possède un contrat de reprise, elle doit être stockée et exécutée en arrière-plan. L’adaptateur ParseIncidentRequest ci-dessus convertit l’entrée du protocole. Démarrez l’hôte avec dotnet run --urls http://localhost:8088, puis testez depuis un autre terminal :

$body = @{
  input = '{"incidentId":"INC-42","summary":"Les requêtes retournent 503."}'
  store = $true
  background = $true
} | ConvertTo-Json

Invoke-RestMethod -Method Post `
  -Uri "http://localhost:8088/responses" `
  -ContentType "application/json" `
  -Body $body

Conservez l’identifiant response.id retourné par le serveur, puis interrogez son état depuis le client ou l’interface Foundry. Ne fabriquez pas cet identifiant et ne l’interprétez pas comme une autorisation : il sert à retrouver l’opération, pas à prouver l’identité de la personne qui l’observe.

Important : le raccourci workflow.AsAIAgent() convient au scénario minimal ci-dessus. Pour un workflow qui attend une réponse humaine, traite plusieurs tours ou doit reprendre entre plusieurs processus, suivez le modèle de factory de workflow « native workflow » de la documentation Foundry : elle reconstruit un graphe neuf à chaque requête tout en gardant les identités stables. Cela évite de partager un état mutable en mémoire entre conversations.

3. Rendre une opération externe réellement idempotente

Ne placez jamais un appel de déploiement ou de ticketing directement dans un prompt. Encapsulez-le dans une interface qui reçoit une clé d’idempotence, puis demandez au système cible de mémoriser cette clé avec le résultat de l’opération.

namespace ResilientIncidentAgent;

public interface IChangeGateway
{
    Task<string> CreateTicketAsync(
        string idempotencyKey,
        IncidentReport report,
        CancellationToken cancellationToken);
}

public sealed class CreateIncidentTicket(IChangeGateway gateway)
{
    public Task<string> ExecuteAsync(
        string workflowRunId,
        IncidentReport report,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(workflowRunId))
        {
            throw new ArgumentException("Le run est requis.", nameof(workflowRunId));
        }

        string key = $"incident-ticket:{workflowRunId}:v1";
        return gateway.CreateTicketAsync(key, report, cancellationToken);
    }
}

La valeur v1 est volontaire : changez-la seulement si l’opération métier doit réellement devenir une nouvelle commande. Côté IChangeGateway, utilisez une contrainte d’unicité ou le mécanisme d’idempotence de l’API cible. Un HashSet en mémoire ne suffit pas, car il disparaît précisément lors du redémarrage qui déclenche la reprise.

Évitez aussi de sauver un checkpoint après une action en supposant que cela la rend atomique. La fenêtre entre l’effet de bord et l’écriture du checkpoint existe toujours. La bonne combinaison est : checkpoint fréquent + clé d’idempotence durable + opération réversible ou compensable lorsque c’est possible.

4. Tester les garanties sans modèle ni cloud

Les tests de pull request doivent vérifier les règles qui ne dépendent pas d’un fournisseur. Ajoutez un projet xUnit, référencez l’application, puis testez l’entrée et la clé d’idempotence avec un faux gateway :

dotnet new xunit --framework net10.0 --output ../ResilientIncidentAgent.Tests
dotnet add ../ResilientIncidentAgent.Tests reference .
dotnet restore ../ResilientIncidentAgent.Tests/ResilientIncidentAgent.Tests.csproj --use-lock-file

Le projet de tests est voisin du projet web pour éviter que les globs C# de l’application compilent les fichiers xUnit. Versionnez les deux fichiers packages.lock.json. La CI ci-dessous suppose que le dépôt contient ces deux dossiers à sa racine.

using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using ResilientIncidentAgent;

namespace ResilientIncidentAgent.Tests;

public sealed class CreateIncidentTicketTests
{
    [Fact]
    public async Task Routes_the_request_json_through_the_agent()
    {
        AIAgent agent = IncidentWorkflow.Create().AsAIAgent(
            includeWorkflowOutputsInResponse: true);

        AgentResponse response = await agent.RunAsync(
            """{"incidentId":"INC-42","summary":"Les requêtes retournent 503."}""");

        Assert.Contains("INC-42", response.Text);
        Assert.Contains("503", response.Text);
    }

    [Fact]
    public async Task Reuses_the_same_key_when_a_step_is_replayed()
    {
        var gateway = new RecordingGateway();
        var action = new CreateIncidentTicket(gateway);
        var report = new IncidentReport("INC-42", "Les sondes échouent.");

        await action.ExecuteAsync("run-123", report, CancellationToken.None);
        await action.ExecuteAsync("run-123", report, CancellationToken.None);

        Assert.Equal(
            ["incident-ticket:run-123:v1", "incident-ticket:run-123:v1"],
            gateway.Keys);
    }

    private sealed class RecordingGateway : IChangeGateway
    {
        public List<string> Keys { get; } = [];

        public Task<string> CreateTicketAsync(
            string idempotencyKey,
            IncidentReport report,
            CancellationToken cancellationToken)
        {
            Keys.Add(idempotencyKey);
            return Task.FromResult("TICKET-99");
        }
    }
}

Ce test ne prouve pas que le système tiers déduplique : il prouve que le workflow lui envoie une clé stable lorsqu’une étape est rejouée. Ajoutez ensuite un test d’intégration contre un environnement synthétique qui vérifie qu’une seconde requête avec la même clé retourne le même résultat, sans créer une seconde ressource.

5. Tester une reprise contrôlée avant la production

Dans un environnement de test Foundry, lancez une demande store: true et background: true, puis interrompez intentionnellement le processus après un checkpoint connu. Redémarrez l’hôte avec le même déploiement et observez la même response.id jusqu’à sa conclusion. Les contrôles importants sont :

  1. les éléments déjà produits n’apparaissent ni manquants ni dupliqués ;
  2. l’étape redémarre à la frontière de checkpoint attendue ;
  3. une écriture externe conserve le même résultat grâce à sa clé d’idempotence ;
  4. un nouveau déploiement conserve le nom du workflow et les identifiants des exécuteurs ;
  5. un client non autorisé ne peut pas consulter ou poursuivre la réponse d’un autre utilisateur.

Les décisions d’architecture du projet Agent Framework décrivent aussi les essais de reprise : interruption forcée, restauration de session et vérification qu’un décompte final ne comporte aucune valeur manquante ou répétée. Reprenez le principe avec un effet de bord synthétique avant de connecter Jira, GitHub ou une plateforme de déploiement.

6. Séparer la CI déterministe du test d’intégration privilégié

Une pull request ne devrait jamais obtenir une identité capable d’écrire dans Foundry ou dans un outil de production. Utilisez deux niveaux :

name: Validate resilient workflow

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: 10.0.x
      - run: dotnet restore ResilientIncidentAgent.Tests/ResilientIncidentAgent.Tests.csproj --locked-mode
      - run: dotnet test ResilientIncidentAgent.Tests/ResilientIncidentAgent.Tests.csproj --no-restore --configuration Release

  recovery-integration:
    if: github.event_name == 'push'
    needs: unit
    environment: agent-integration
    permissions:
      contents: read
      id-token: write
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      - run: ./scripts/test-recovery.ps1
        shell: pwsh

Le premier job compile et exécute les règles pures. Le second doit cibler un projet Foundry isolé, créer seulement des données de test et détruire ou expirer ces données selon une politique connue. La fédération OIDC évite de conserver un secret client longue durée dans GitHub Actions ; elle ne dispense pas de restreindre les rôles au dépôt, à la branche et à l’environnement attendu.

Points de vigilance

  • background sans store n’a pas de contrat de reprise. Envoyez explicitement les deux valeurs et testez-les dans le client réel.
  • Le premier enregistrement décide de la résilience. Configurez ResilientBackground lors du premier AddFoundryResponses, avant qu’un autre module n’enregistre un serveur Responses.
  • Les checkoints ne sont pas compatibles par magie. Versionnez vos changements de graphe et démarrez une nouvelle lignée pour une migration qui renomme une étape.
  • Une reprise n’est pas exactement une fois. Limitez les effets de bord, conservez une clé idempotente durable et prévoyez une compensation.
  • Les sessions et checkpoints peuvent contenir des données sensibles. Définissez une rétention, des contrôles d’accès, une redaction des traces et un nettoyage des environnements de test.
  • Les réponses passées restent non fiables. Une sortie de modèle ou un commentaire d’incident ne doit jamais devenir une URL, une commande ou un paramètre de déploiement sans validation typée et liste blanche côté serveur.

Sources

Une réponse longue n’est opérationnelle que si son redémarrage est prévu avant l’incident. Les réponses résilientes Foundry réduisent la perte de progrès ; des identités de workflow stables, des tests de redémarrage et l’idempotence des effets de bord empêchent que cette reprise ne crée un nouveau problème.

Mots-clés :.NETC#AI AgentsDevOpsMicrosoft FoundryRésilience
Y

Yva Hajatiana

Lead développeur .NET à Paris et architecte logiciel, 10 ans d'expérience sur des SI critiques bancaires (.NET, Angular, systèmes distribués, IAM, DevOps).