MICROSOFT AGENT FRAMEWORK · 0.16.0-rc.1
Faits contrôlés et dérive du contexte
Admettre, corriger et oublier des assertions sans recycler les réponses du modèle.
Objectifs
- Faits canoniques et historique
- Correction et oubli coordonnés
- Contexte injecté et dérive
La correction doit changer un état
Une conversation mentionne Reporting et PostgreSQL, puis corrige vers Billing et SQL Server 2022. Conserver les deux versions dans l’historique laisse au modèle le choix de la priorité. EP013 montre ce glissement : une réponse plausible peut ressusciter une hypothèse abandonnée ou oublier une interdiction ancienne. La mémoire contrôlée rend la correction explicite : une clé est remplacée, sa provenance reste visible.
Un contrat volontairement étroit
Les seules déclarations admises suivent key=value, séparées par point-virgule ou retour à la ligne. Clés autorisées : product, module, priority, database, constraint, customer, version, environment, symptom, log. Le writer propose des candidats typés sans session, depuis le seul message User courant. L’hôte vérifie l’ensemble des clés et valeurs contre les déclarations explicites et exige une citation exacte. Une question ou une réponse de l’assistant n’est jamais promue automatiquement en fait.
var declarations = ExplicitAssertions.Parse(userText);
if (response.Result.Facts is null || response.Result.Facts.Count != declarations.Count
|| response.Result.Facts.Any(f => f is null || !declarations.TryGetValue(f.Key, out var value) || value != f.Value))
throw new InvalidDataException("Extraction does not match the explicit key=value assertions. No semantic guess is admitted.");
memory.Apply(turn, response.Result.Facts, DateTimeOffset.UtcNow);
Code source · Maf.Memory · Dossier
L’admission est atomique : une clé inconnue, un doublon de candidat, une preuve absente ou une mauvaise association sémantique rejette tout le lot. La dernière déclaration d’une même clé dans le message fait foi. La citation prouve que l’utilisateur a écrit ces mots ; elle ne prouve pas la vérité de son assertion. Si l’extraction échoue, les faits précédents restent présents et le diagnostic annonce l’absence de fallback. Contrôler mem avant de poursuivre après une correction rejetée.
Provenance, bornes et obsolescence
Chaque fait porte Key, Value, Evidence, Turn et RecordedAt UTC. Le store et chaque extraction sont limités à cinq faits, les valeurs à 256 caractères, les preuves à 512 et le message à 4096. Une nouvelle clé évince le fait au tour le plus ancien ; une égalité est départagée par clé ordinale. Une correction remplace la même clé. Un ticket Epoch/Number empêche une extraction ancienne ou antérieure à forget de réintroduire une donnée.
public void Apply(MemoryTurn turn, IReadOnlyList<FactCandidate>? candidates, DateTimeOffset recordedAt)
{
lock (_gate)
{
if (turn.Epoch != _epoch || turn.Number != _turn)
throw new InvalidOperationException("Memory update is stale or belongs to a forgotten conversation.");
if (candidates is null || candidates.Count > 5)
throw new ArgumentException("Extraction must contain at most five facts.");
var keys = new HashSet<string>(StringComparer.Ordinal);
foreach (var fact in candidates)
{
if (fact is null || !AllowedKeys.Contains(fact.Key ?? "") || !keys.Add(fact.Key!)
|| string.IsNullOrWhiteSpace(fact.Value) || fact.Value.Length > 256
|| string.IsNullOrWhiteSpace(fact.Evidence) || fact.Evidence.Length > 512
|| !turn.UserText.Contains(fact.Evidence, StringComparison.Ordinal)
|| !fact.Evidence.Contains(fact.Value, StringComparison.Ordinal))
throw new ArgumentException("Invalid fact: use a unique allowed key and a value quoted in current user evidence.");
}
foreach (var fact in candidates)
_facts[fact.Key] = new(fact.Key, fact.Value, fact.Evidence, turn.Number, recordedAt);
while (_facts.Count > 5)
{
var oldest = _facts.Values.OrderBy(f => f.Turn).ThenBy(f => f.Key, StringComparer.Ordinal).First();
_facts.Remove(oldest.Key);
Injection manuelle ou provider natif
- Message User → réponse métier
- Succès → extraction du seul User externe → validation hôte
- WorkingMemory → injection au prochain appel des deux agents
- forget [key] → retirer les faits visés + remplacer les deux sessions
Demo150 ajoute ToPromptBlock au message avant RunAsync, puis lance l’extraction après la réponse. Demo160 fournit le même bloc par AIContextProvider avant l’appel et extrait après succès les messages User dont la source est External. Il exclut ainsi les réponses générées et son propre contexte injecté. La nouvelle assertion se trouve déjà dans le message courant ; le store corrigé servira aux appels suivants. Deux sessions métier distinctes partagent ce seul store, sans fusionner tous leurs historiques.
protected override async ValueTask StoreAIContextAsync(InvokedContext context,
CancellationToken cancellationToken = default)
{
if (context.InvokeException is not null)
return;
foreach (var message in context.RequestMessages.Where(m => m.Role == Microsoft.Extensions.AI.ChatRole.User
&& m.GetAgentRequestMessageSourceType() == AgentRequestMessageSourceType.External))
{
if (!string.IsNullOrWhiteSpace(message.Text))
report(await extraction.UpdateAsync(message.Text, cancellationToken));
Comparer les politiques sans annoncer de gagnant
Demo170 exécute une chronologie fixe, pas l’ancien menu vidéo : historique complet, trois derniers tours, résumé généré non vérifié, faits bornés et compaction native de requête MAF. Un résumé de plus de 700 caractères ou vide arrête la comparaison sans texte de secours. La fenêtre peut perdre une interdiction ancienne ; le résumé peut la déformer ; cinq faits peuvent évincer une information encore utile. La provenance peut rendre les faits plus volumineux qu’un dialogue très court.
CompactionProvider réduit la vue envoyée au modèle en conservant l’historique source sérialisable. AsChatReducer réduit l’historique retenu après ajout : le snapshot ne contient alors que la liste réduite. Ce sont deux effets différents. Une estimation caractères/4 ne mesure ni les tokens facturés ni le coût total ; inclure les appels du writer et du résumeur, et conserver unknown si le fournisseur n’annonce pas son usage.
public static IChatClient CreateRequestView(IChatClient chat)
{
#pragma warning disable MAAI001
return chat.AsBuilder().UseAIContextProviders(new CompactionProvider(
new SlidingWindowCompactionStrategy(CompactionTriggers.TurnsExceed(3), minimumPreservedTurns: 3))).Build();
#pragma warning restore MAAI001
}
/// <summary>Reduces retained history after each completed exchange; serialization saves the reduced list.</summary>
public static InMemoryChatHistoryProvider CreateBoundedHistory()
{
#pragma warning disable MAAI001
return new(new InMemoryChatHistoryProviderOptions
{
ChatReducer = new SlidingWindowCompactionStrategy(CompactionTriggers.TurnsExceed(3), minimumPreservedTurns: 3).AsChatReducer(),
ReducerTriggerEvent = InMemoryChatHistoryProviderOptions.ChatReducerTriggerEvent.AfterMessageAdded
Corriger puis oublier
Dans Demo150 puis Demo160, saisir module=Reporting; database=PostgreSQL, mem, puis module=Billing; database=SQL Server 2022 et mem. Vérifier deux clés corrigées et leurs citations. Demander Where can I find logs? pour emprunter l’autre agent ; vérifier mem après cet appel. forget module doit conserver database mais remplacer les deux historiques. forget retire tous les faits ; une question suivante ne doit pas les recréer par inférence. Les faits ne sont pas persistés et disparaissent à la fermeture.
dotnet test tests/Maf.Memory.Tests/Maf.Memory.Tests.csproj -m:1 -c Release --no-build --no-restoreLa mémoire n’autorise pas une action
Ces assertions utilisateur restent des données non vérifiées, étiquetées comme telles. Elles ne remplacent ni référentiel métier, ni authentification, ni autorisation d’outil. L’oubli porte sur le store actif et les sessions des deux agents ; il ne purge pas les sauvegardes externes. Une application plus large doit définir propriétaire, expiration, révocation et revue des faits. Les variantes live demandent un chat capable de respecter les contrats ; les tests scriptés n’en qualifient aucun.
Fiches et contexte documentaire
Prérequis
Pour poursuivre
Précédent
Suivant
