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
- Recherche et citation vérifiée
- Preuves liées au brouillon exact
- Approbation externe et décision durable
- Effet relu, refus et rejeu
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.
- Corpus admissible au 23 octobre 2026 → recherche vectorielle SQLite
- AnswerGate → citation exacte → BacklogDraft validé
- ApprovalTicket → RequestPort → checkpoint Waiting
- 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));
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));
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.");
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));
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);
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
| Mécanisme | Portée |
|---|---|
| vectors.db | Index vectoriel ; pas une décision |
| business.db | Brouillon, première décision et item métier |
| checkpoints + waiting.json | Frontière native de reprise du graphe |
| evidence.json | Diagnostic ; ne commande pas la reprise |
| host.lock | Exclusion 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
