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
- Superstep et frontière du checkpoint
- Requête externe et identité du graphe
- Décision liée au brouillon immuable
- Commit, crash et rejeu sans exactly-once distribué
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.
| Mécanisme | Portée |
|---|---|
| Demo300 | RequestPort réel ; décision CLI fournie immédiatement dans le processus vivant. |
| Demo310 | CreateInMemory ; graphe reconstruit, manager conservé dans le même processus. |
| Demo320 | Checkpoints 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();
Code source · Maf.Durable · Dossier
- shared/Maf.Durable/Maf.Durable.csproj
- demos/Demo300_ExternalRequest/Demo300.App.csproj
- demos/Demo310_CheckpointResume/Demo310.App.csproj
- demos/Demo320_DurableApproval/Demo320.App.csproj
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.");
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));
Code source · Maf.Durable · Dossier
- shared/Maf.Durable/Maf.Durable.csproj
- demos/Demo300_ExternalRequest/Demo300.App.csproj
- demos/Demo310_CheckpointResume/Demo310.App.csproj
- demos/Demo320_DurableApproval/Demo320.App.csproj
- start → proposition SQLite → RequestInfoEvent → checkpoint → waiting.json → fin du processus
- resume → même graphe + checkpoint → même requête réémise
- Vérifier les identités → enregistrer la décision SQLite → SendResponseAsync
- 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.
| Mécanisme | Portée |
|---|---|
| Approbation d’outil Demo110/120 | Autorise une invocation selon le contrat de l’hôte actif ; ne constitue pas le protocole de reprise de Demo320. |
| Décision métier Demo320 | Proposition 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);
Code source · Maf.Durable · Dossier
- shared/Maf.Durable/Maf.Durable.csproj
- demos/Demo300_ExternalRequest/Demo300.App.csproj
- demos/Demo310_CheckpointResume/Demo310.App.csproj
- demos/Demo320_DurableApproval/Demo320.App.csproj
| Mécanisme | Portée |
|---|---|
| Après attente publiée | Waiting, zéro effet ; reprendre avec une décision valide. |
| Après décision, avant effet | Approved ou Rejected ; renvoyer la même décision. |
| Pendant transaction métier | Insertion et Completed sont annulés ensemble en cas d’échec. |
| Après commit, avant fin moteur | Completed 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-restoreUne 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
