e-naxosMAF COURSE / COURS
English

MICROSOFT AGENT FRAMEWORK · 0.16.0-rc.1

Attente, checkpoint et effet idempotent

Conserver une demande pendante et rejouer une décision sans ajouter un second effet SQLite.

Objectifs

Attendre au-delà de la console

Une proposition de pilote régional attend une décision métier. Fermer la console ne doit pas perdre la demande ; une réponse répétée ne doit pas doubler la ligne créée. Video3 apportait le modèle mental de suspension/reprise. Le scénario actuel lie un BacklogDraft immuable à une opération et inspecte un véritable stockage SQLite. Session conversationnelle, checkpoint moteur et état métier ont des responsabilités distinctes.

Trois niveaux de reprise
MécanismePortée
Demo300RequestPort réel ; décision CLI fournie immédiatement dans le processus vivant.
Demo310CreateInMemory ; graphe reconstruit, manager conservé dans le même processus.
Demo320Checkpoints JSON sur disque, waiting.json et business.db ; start puis resume dans des processus distincts.

Capturer la bonne frontière

PrepareApprovalV1 valide et enregistre la proposition. BusinessApprovalV1 est un RequestPort<ApprovalTicket, ApprovalDecision>. ApplyApprovalV1 applique la décision. Le moteur émet RequestInfoEvent pour l’attente ; la CLI fournit la réponse via CreateResponse puis SendResponseAsync. La partie externe est simulée par le choix CLI ; la requête, la corrélation et le moteur sont réels.

var prepare = new Prepare(store);
var port = RequestPort.Create<ApprovalTicket, ApprovalDecision>("BusinessApprovalV1");
var apply = new Apply(store, failAfterWrite);
return new WorkflowBuilder(prepare).AddEdge(prepare, port).AddEdge(port, apply)
    .WithOutputFrom(apply).Build();

shared/Maf.Durable/ApprovalWorkflow.cs · L20–L24 · 9a895c94534c

Code source · Maf.Durable · Dossier

Un superstep est un cycle d’exécution et de synchronisation du moteur sur les messages et destinataires en attente. Il ne s’agit pas d’un groupe métier librement choisi. CaptureWaitingAsync observe d’abord RequestInfoEvent, puis conserve le checkpoint du SuperStepCompletedEvent suivant. Un checkpoint antérieur peut ne pas contenir la demande. waiting.json est publié par fichier temporaire puis renommage seulement après cette capture.

string? requestId = null;
await foreach (var evt in run.WatchStreamAsync(cancellationToken))
{
    Console.WriteLine("Event: " + evt.GetType().Name);
    if (evt is WorkflowErrorEvent error) throw new InvalidOperationException("Workflow failed.", error.Exception);
    if (evt is RequestInfoEvent request)
    {
        var ticket = request.Request.Data.As<ApprovalTicket>();
        if (ticket?.OperationId != operationId) throw new InvalidOperationException("Unexpected pending operation.");
        requestId = request.Request.RequestId;
    }
    if (requestId is not null && evt is SuperStepCompletedEvent { CompletionInfo.Checkpoint: { } checkpoint })
        return new(ApprovalWorkflow.Version, checkpoint.SessionId, checkpoint.CheckpointId, operationId, requestId);
}
throw new InvalidOperationException("No checkpoint containing the pending request was observed.");

shared/Maf.Durable/ApprovalDemo.cs · L78–L92 · 9a895c94534c

Code source · Maf.Durable · Dossier

Reconstruire et corréler la demande

waiting.json garde GraphVersion, SessionId, CheckpointId, OperationId et RequestId. Le premier couple localise le checkpoint dans le store natif, OperationId identifie le métier et RequestId identifie la requête moteur. Au resume, l’hôte reconstruit le graphe avec les mêmes identités fixes et contrats, puis appelle ResumeStreamingAsync. La demande pendante réapparaît avec le même RequestId ; l’hôte vérifie cette identité et OperationId avant de répondre. Il ne recrée pas une demande depuis une chaîne console.

if (ticket?.OperationId != operationId || decision.OperationId != operationId
    || (requestId is not null && request.Request.RequestId != requestId))
    throw new ArgumentException("The response does not match the pending request.");
store.RecordDecision(decision);
Console.WriteLine("Responding: " + request.Request.RequestId);
await run.SendResponseAsync(request.Request.CreateResponse(decision));

shared/Maf.Durable/ApprovalDemo.cs · L107–L112 · 9a895c94534c

Code source · Maf.Durable · Dossier

Reprise à partir de l’attente
  1. start → proposition SQLite → RequestInfoEvent → checkpoint → waiting.json → fin du processus
  2. resume → même graphe + checkpoint → même requête réémise
  3. Vérifier les identités → enregistrer la décision SQLite → SendResponseAsync
  4. ApplyApprovalV1 → transaction SQLite → résultat ; PrepareApprovalV1 ne doit pas être rejoué

Lier décision et proposition immuable

OperationId est créé une seule fois au start et lié au JSON de la proposition dans Approvals. Réutiliser cette clé pour un autre brouillon échoue. La première décision valide est persistée avant son envoi au moteur. Seuls approve et reject, avec un commentaire non blanc de un à 500 caractères, sont admis. Un rejeu doit reproduire exactement choix et commentaire ; une décision contradictoire ou modifiée échoue. Un refus reste final et ne crée aucune ligne ApprovedBacklogItems.

Approbation en processus et décision durable
MécanismePortée
Approbation d’outil Demo110/120Autorise une invocation selon le contrat de l’hôte actif ; ne constitue pas le protocole de reprise de Demo320.
Décision métier Demo320Proposition liée à OperationId, décision persistée, requête corrélée et effet local idempotent.

La CLI est une source de décision de confiance dans cet exemple. Aucun login, rôle de relecteur ou validation métier humaine indépendante n’est établi. Ce protocole n’utilise pas l’approbation native des function tools de MAF.

Commit, arrêt et rejeu

Apply enregistre à nouveau la décision pour vérifier son identité, puis insère ApprovedBacklogItems et marque Approvals.Completed dans une transaction SQLite. La contrainte UNIQUE sur OperationId protège la ligne métier même lors d’appels concurrents au store. Le rejeu retourne le même ItemId. status joint la ligne et affiche StoredItem : c’est une lecture indépendante du résultat du moteur. Un événement de succès seul ne suffit pas.

public ApprovalState Apply(ApprovalDecision decision)
{
    RecordDecision(decision);
    using var connection = Open();
    using var transaction = connection.BeginTransaction();
    using var command = connection.CreateCommand();
    command.Transaction = transaction;
    command.CommandText = """
        INSERT INTO ApprovedBacklogItems (OperationId,Draft,CreatedUtc)
        SELECT OperationId,Draft,$utc FROM Approvals WHERE OperationId=$id AND Status IN ('Approved','Completed')
        ON CONFLICT(OperationId) DO NOTHING;
        UPDATE Approvals SET Status='Completed' WHERE OperationId=$id AND Status='Approved';
        """;
    command.Parameters.AddWithValue("$id", decision.OperationId);
    command.Parameters.AddWithValue("$utc", DateTimeOffset.UtcNow.ToString("O"));
    command.ExecuteNonQuery();
    transaction.Commit();
    return Read(decision.OperationId);

shared/Maf.Durable/ApprovalStore.cs · L80–L97 · 9a895c94534c

Code source · Maf.Durable · Dossier

Fenêtres d’interruption
MécanismePortée
Après attente publiéeWaiting, zéro effet ; reprendre avec une décision valide.
Après décision, avant effetApproved ou Rejected ; renvoyer la même décision.
Pendant transaction métierInsertion et Completed sont annulés ensemble en cas d’échec.
Après commit, avant fin moteurCompleted et une ligne ; rejouer le checkpoint d’attente, même ItemId.

Provoquer un arrêt après le commit

La fiche Demo320 et l’exercice détaillent start, status, resume et le rejeu. Sur une nouvelle opération, ajouter approve "External business decision." --fail-after-write à resume. Le programme retourne 1 après le commit ; status doit pourtant lire Completed et un StoredItem. Relancer avec le même commentaire sans le drapeau doit terminer avec le même ItemId et une seule ligne. Le reçu d’attente est volontairement conservé après achèvement pour tester ce cas.

dotnet test tests/Maf.Durable.Tests/Maf.Durable.Tests.csproj -m:1 -c Release --no-build --no-restore

Une garantie locale précisément délimitée

Conserver ensemble checkpoints, waiting.json et business.db. Un lease fichier exclut plusieurs hôtes CLI sur le même répertoire ; les tests concurrents du store ne démontrent pas un ordonnanceur distribué. Ces fichiers et SQLite ne forment pas une transaction commune. L’arrêt pendant la publication initiale, la corruption, la perte de fichiers, une coupure électrique et les systèmes de fichiers distants ne sont pas qualifiés. Si start échoue sans reçu, préserver le répertoire pour inspection et utiliser un nouveau répertoire.

L’unicité porte sur l’effet SQLite local. Elle ne promet pas une exécution unique de chaque exécuteur. Paiement, email ou ticket distant exigeraient leur propre clé d’idempotence durable ou un contrat outbox/récepteur. Modifier version, identités ou contrats du graphe demande une migration ; la version inconnue est refusée. Authentification, expiration, amendement, chiffrement et rétention restent hors de cet exemple.

Exécuter les trois niveaux

Prérequis

Pour poursuivre

Précédent

Suivant

lot009-workflows-1 · 2026-10-24 · 9a895c94534cd5fc6f3da883e37e5f7856e588c1