e-naxosMAF COURSE / COURS
English

MICROSOFT AGENT FRAMEWORK · 0.16.0-rc.1

Action documentée intégrée

Faire aboutir une preuve documentaire admissible à une décision externe et à un effet SQLite unique après reprise.

Objectifs

Le dernier passage du parcours principal

Demo470 rassemble les acquis du cours autour d’une question fixe : Where can I export CSV in Reporting? Le problème ne consiste plus seulement à obtenir une réponse. Il faut montrer quel passage la soutient, quel brouillon a été présenté, qui décide dans l’hôte et quelle ligne existe après redémarrage. Le parcours principal aboutit ici ; Demo480 et Demo490 prolongent la réflexion sans conditionner cette manipulation.

Du document à la ligne métier
  1. Corpus admissible au 23 octobre 2026 → recherche vectorielle SQLite
  2. AnswerGate → citation exacte → BacklogDraft validé
  3. ApprovalTicket → RequestPort → checkpoint Waiting
  4. Commande externe approve/reject → reprise → lecture métier

Admettre la preuve avant de demander une décision

La politique du corpus filtre les sources selon la date de référence, le périmètre course et leur admissibilité avant classement. Microsoft.Data.Sqlite conserve les vecteurs denses ; le cosinus exact est calculé en C#. La fixture utilise deux dimensions contrôlées et sélectionne ACME-EXPORT. Ce sont de vrais vecteurs recherchés dans SQLite, avec des entrées choisies pour enseigner la chaîne, sans mesure de qualité des embeddings.

Console.WriteLine("Profile=fixture; controlled dense vectors and answer; no model qualification");
var manifest = IndexManifest.Create(corpus, "fixture", "fixture", "controlled-2d-v1", 2);
var chunks = CorpusPolicy.Chunks(corpus);
vectors.Rebuild(corpus, manifest, chunks.Select(c => c.Source.Id == "ACME-EXPORT"
    ? new float[] { 1, 0 } : new float[] { 0, 1 }).ToArray());
var hits = vectors.Search(corpus, manifest, [1, 0], asOf, "course");
var selected = hits.Single(h => h.Chunk.Source.Id == "ACME-EXPORT");
answer = AnswerGate.Validate(new(true, [new(args.Contains("--invalid-evidence")
    ? "INVENTED#0" : selected.Chunk.Id, selected.Chunk.Text)]), new(question, asOf, hits, null));

demos/Demo470_DocumentedApproval/Program.cs · L53–L61 · 9a895c94534c

Code source · Demo470 · Dossier

AnswerGate exige une proposition exploitable, une preuve issue des résultats admissibles et une citation exacte d’au moins vingt caractères après Trim. Le texte final est reconstruit depuis les couples identifiant/citation ; une paraphrase libre ne devient pas une preuve. --invalid-evidence remplace l’identifiant par INVENTED#0 : le rejet intervient avant l’enregistrement de l’approbation. Une citation authentique prouve sa présence dans le passage, pas la vérité universelle du document ni la justesse de toute interprétation.

Figer la proposition qui sera approuvée

if (!answer.Accepted) throw new InvalidOperationException(answer.Text);
var ticket = new ApprovalTicket(Guid.NewGuid().ToString("N"), new BacklogDraft(
    "Review Reporting CSV export", "Question", "medium", "Reporting", answer.Text, []));
if (ticket.Draft.Validate().Length != 0) throw new InvalidOperationException("Evidence exceeds the business contract.");
await using var run = await InProcessExecution.RunStreamingAsync(ApprovalWorkflow.Build(store), ticket,
    manager, cancellationToken: timeout.Token);
var receipt = await ApprovalDemo.CaptureWaitingAsync(run, ticket.OperationId, timeout.Token);
File.WriteAllText(Path.Combine(directory, "evidence.json"), JsonSerializer.Serialize(answer));
File.WriteAllText(receiptPath, JsonSerializer.Serialize(receipt));

demos/Demo470_DocumentedApproval/Program.cs · L63–L71 · 9a895c94534c

Code source · Demo470 · Dossier

public void Register(ApprovalTicket ticket)
{
    if (string.IsNullOrWhiteSpace(ticket.OperationId) || ticket.Draft.Validate().Length != 0)
        throw new ArgumentException("A valid operation and actionable draft are required.");
    using var connection = Open();
    using var command = connection.CreateCommand();
    command.CommandText = "INSERT INTO Approvals VALUES ($id,$draft,NULL,'Waiting') ON CONFLICT DO NOTHING";
    command.Parameters.AddWithValue("$id", ticket.OperationId);
    command.Parameters.AddWithValue("$draft", JsonSerializer.Serialize(ticket.Draft));
    command.ExecuteNonQuery();
    if (JsonSerializer.Serialize(Read(ticket.OperationId).Draft) != JsonSerializer.Serialize(ticket.Draft))
        throw new InvalidOperationException("The operation key is already bound to another draft.");

shared/Maf.Durable/ApprovalStore.cs · L39–L50 · 9a895c94534c

Code source · Maf.Durable · Dossier

L’hôte crée OperationId puis BacklogDraft avec answer.Text dans Summary. Validate protège le contrat métier avant le graphe. Register lie la clé au JSON exact du brouillon et refuse de la réutiliser pour un autre contenu. evidence.json reste une copie de diagnostic : le modifier ne réécrit pas le Draft déjà enregistré dans business.db. L’immuabilité est une règle de cette API sur un stockage hôte de confiance ; elle ne résiste pas à un administrateur qui modifie les fichiers.

Le RequestPort suspend le graphe sans écrire d’item approuvé. waiting.json conserve GraphVersion, SessionId, CheckpointId, OperationId et RequestId. Ces identifiants servent à retrouver la même demande observée, pas à recréer une proposition approximative depuis le dernier texte du modèle. La décision externe est une commande explicite de l’opérateur ; cette console ne vérifie aucune identité ni rôle d’approbateur.

Reprendre le graphe et vérifier l’effet

var saved = JsonSerializer.Deserialize<WaitingReceipt>(File.ReadAllText(receiptPath))!;
if (saved.GraphVersion != ApprovalWorkflow.Version) throw new InvalidOperationException("Graph version mismatch.");
if (args[0] == "resume")
{
    if (args.Length != 3 || args[2] is not ("approve" or "reject"))
        throw new ArgumentException("An explicit approve or reject decision is required.");
    var decision = new ApprovalDecision(saved.OperationId, args[2], "External business decision.");
    await using var run = await InProcessExecution.ResumeStreamingAsync(ApprovalWorkflow.Build(store),
        new CheckpointInfo(saved.SessionId, saved.CheckpointId), manager, timeout.Token);
    await ApprovalDemo.RespondAsync(run, store, saved.OperationId, saved.RequestId, decision, timeout.Token);
}
Print(store.Read(saved.OperationId));

demos/Demo470_DocumentedApproval/Program.cs · L77–L88 · 9a895c94534c

Code source · Demo470 · Dossier

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

Le nouveau processus reconstruit le graphe de même version et restaure le checkpoint. La réponse doit correspondre à l’opération et à la demande attendues. RecordDecision accepte approve ou reject avec commentaire borné ; la première décision persistée gagne et seule sa répétition exacte est admise. Un refus n’est pas une attente à convertir ensuite en approbation : créer une nouvelle proposition pour une nouvelle décision.

Apply enregistre d’abord la décision, puis une transaction insère ApprovedBacklogItems depuis le brouillon persisté et marque Completed. La contrainte UNIQUE(OperationId) évite le second effet. Si l’insertion échoue, la transaction annule l’effet et la complétion, tandis que la décision peut rester Approved et être réessayée. Si le processus échoue après commit, une reprise rend la même ligne. Relire ItemId, StoredItem et le nombre de lignes ; une sortie de workflow seule ne suffit pas.

Attribuer chaque preuve à son mécanisme

Objets persistés et responsabilités
MécanismePortée
vectors.dbIndex vectoriel ; pas une décision
business.dbBrouillon, première décision et item métier
checkpoints + waiting.jsonFrontière native de reprise du graphe
evidence.jsonDiagnostic ; ne commande pas la reprise
host.lockExclusion de processus sur cet hôte

Chaque phase émet des traces restreintes. OperationId relie durablement les phases ; il ne transforme pas leurs traces en trace distribuée continue. Demo470 ne produit pas le journal transactionnel ActionCommitted de Demo430. Ses preuves d’effet sont les lignes de business.db. La recherche, les fichiers de checkpoint et la transaction métier ne forment pas une transaction unique entre modules.

Ce que cette réussite laisse à décider

La fixture ne qualifie aucun modèle. --live reconstruit l’index avec le fournisseur d’embeddings configuré puis appelle l’agent natif de réponse ; un refus ou une panne reste visible, sans repli fixture. Le profil granite4:3b candidat/juge reste non qualifié et la validation métier humaine indépendante reste non établie. Cette console ne publie aucune route HTTP, aucun formulaire Web et aucun service d’approbation authentifié. Les interfaces Demo410 sont une application séparée.

Exécuter puis approfondir

Prérequis

Pour poursuivre

Précédent

lot013-integration-1 · 2026-10-24 · 9a895c94534cd5fc6f3da883e37e5f7856e588c1