MICROSOFT AGENT FRAMEWORK · 0.16.0-rc.1
Graphes typés et événements
Décrire le chemin par des messages typés et vérifier la sortie métier du graphe.
Objectifs
- Messages et branches typés
- Agent natif dans le graphe
- Événements, rôles et sortie terminale
Qui choisit le chemin ?
Une demande de backlog arrive avec un composant inconnu. Le programme doit demander une précision avant toute revue. Video2 posait cette question avec un ancien scénario B2B ; la base actuelle examine un BacklogDraft déjà fourni par l’hôte. Elle ne classe pas du texte libre et ne crée aucune ligne de backlog. Le graphe rend les chemins possibles explicites, puis les événements montrent le chemin effectivement suivi.
| Mécanisme | Portée |
|---|---|
| C# ordinaire | Règles fixes et appels explicites : Demo260. |
| Workflow natif | Exécuteurs, messages et arêtes : Demo230–250. |
| Plan JSON | Donnée proposée par le modèle : Demo270–290. |
Contrats et branches exhaustives
Executor<TInput,TOutput> déclare les messages échangés. AnalyzeDraft copie le tableau des questions, appelle Validate et produit DraftDecision. IsReady dépend des erreurs calculées par l’hôte. Les deux conditions se complètent : un brouillon incomplet atteint ClarifyDraft, un brouillon valide atteint AcceptDraft ou PrepareReview. La clarification produit NeedsClarification comme résultat terminal ; elle ne relance pas automatiquement le dialogue.
var analyze = new AnalyzeDraft();
var clarify = new ClarifyDraft();
var builder = new WorkflowBuilder(analyze)
.AddEdge<DraftDecision>(analyze, clarify, condition: d => d is { IsReady: false });
if (telemetry) builder.WithOpenTelemetry();
if (reviewer is null)
{
var ready = new AcceptDraft();
return builder.AddEdge<DraftDecision>(analyze, ready, condition: d => d is { IsReady: true })
.WithOutputFrom(clarify, ready).Build();
Code source · Maf.Workflows · Dossier
- shared/Maf.Workflows/Maf.Workflows.csproj
- demos/Demo230_TypedWorkflow/Demo230.App.csproj
- demos/Demo240_AgentWorkflow/Demo240.App.csproj
- demos/Demo250_WorkflowEvents/Demo250.App.csproj
- BacklogDraft → AnalyzeDraft → DraftDecision
- Erreurs ou questions → ClarifyDraft → NeedsClarification
- Valide → AcceptDraft → Ready (Demo230)
- Valide → PrepareReview → agent natif → ValidateReview → Reviewed ou Rejected
Insérer un agent natif et garder la frontière
Dans la version liée de MAF, BindAsExecutor adapte un véritable ChatClientAgent au graphe. Les anciennes diapositives qui annonçaient cette intégration pour plus tard sont dépassées pour cette base. PrepareReview envoie ChatMessage puis TurnToken, avec deux déclarations SendsMessage. Le premier message est mis en attente par l’hôte natif ; le token déclenche le tour. Deux livraisons au même exécuteur peuvent donc représenter un seul appel au modèle.
var nativeAgent = reviewer.BindAsExecutor(new AIAgentHostOptions
{
ForwardIncomingMessages = false,
EmitAgentUpdateEvents = false,
EmitAgentResponseEvents = true
});
return builder.AddEdge<DraftDecision>(analyze, prepare, condition: d => d is { IsReady: true })
.AddEdge(prepare, nativeAgent)
.AddEdge(nativeAgent, validate)
.WithOutputFrom(clarify, validate).Build();
Code source · Maf.Workflows · Dossier
- shared/Maf.Workflows/Maf.Workflows.csproj
- demos/Demo230_TypedWorkflow/Demo230.App.csproj
- demos/Demo240_AgentWorkflow/Demo240.App.csproj
- demos/Demo250_WorkflowEvents/Demo250.App.csproj
[SendsMessage(typeof(ChatMessage))]
[SendsMessage(typeof(TurnToken))]
private sealed class PrepareReview() : Executor<DraftDecision>("PrepareReview")
{
public BacklogDraft? Expected { get; private set; }
public override async ValueTask HandleAsync(DraftDecision message, IWorkflowContext context,
CancellationToken cancellationToken = default)
{
Expected = message.Draft;
await context.SendMessageAsync(new ChatMessage(ChatRole.User, JsonSerializer.Serialize(Expected)), cancellationToken: cancellationToken);
await context.SendMessageAsync(new TurnToken(emitEvents: false), cancellationToken: cancellationToken);
Code source · Maf.Workflows · Dossier
- shared/Maf.Workflows/Maf.Workflows.csproj
- demos/Demo230_TypedWorkflow/Demo230.App.csproj
- demos/Demo240_AgentWorkflow/Demo240.App.csproj
- demos/Demo250_WorkflowEvents/Demo250.App.csproj
ForwardIncomingMessages=false empêche le JSON utilisateur de devenir une réponse générée en aval. ValidateReview ne conserve que les messages Assistant puis contrôle JSON, domaines et égalité de tous les faits avec le snapshot hôte. Une priorité modifiée reste rejetée même si le JSON est valide. Reviewed ne signifie ni approbation humaine ni écriture. Cette recopie stricte sert à enseigner la frontière ; une application réelle n’a pas besoin d’un modèle pour recopier un contrat valide.
public static ReviewResult ValidateResponse(string text, BacklogDraft expected)
{
BacklogDraft? draft;
try { draft = JsonSerializer.Deserialize<BacklogDraft>(text, new JsonSerializerOptions(JsonSerializerDefaults.Web)); }
catch (JsonException) { return new("Rejected", null, ["Agent response is not a BacklogDraft JSON object."]); }
if (draft is null) return new("Rejected", null, ["Missing agent draft."]);
var errors = draft.Validate();
if (errors.Length != 0) return new("Rejected", null, errors);
if (draft.Title != expected.Title || draft.Kind != expected.Kind || draft.Priority != expected.Priority
|| draft.Component != expected.Component || draft.Summary != expected.Summary
|| !draft.ClarifyingQuestions.SequenceEqual(expected.ClarifyingQuestions))
return new("Rejected", null, ["The agent changed the supplied business facts."]);
return new("Reviewed", draft, ["Host validation passed; no write tool is exposed."]);
Événements, rôles et preuves
Demo250 consomme tout WatchStreamAsync. ExecutorInvokedEvent décrit les livraisons réelles ; AgentResponseEvent expose la réponse brute ; seul WorkflowOutputEvent portant ReviewResult alimente les résultats métier. Les spans Activity natifs activés par WithOpenTelemetry mesurent causalité et durée, sans collecteur externe. Ni un événement ni un span ne prouve une ligne enregistrée. Le streaming des événements du workflow reste actif alors que les mises à jour de tokens de l’agent sont désactivées.
await foreach (var evt in run.WatchStreamAsync(stop.Token))
{
eventNames.Add(evt.GetType().Name);
if (observe) Console.WriteLine("Event: " + evt.GetType().Name);
if (evt is ExecutorInvokedEvent invoked) path.Add(invoked.ExecutorId);
if (evt is WorkflowErrorEvent error) errors.Add(error.Exception?.Message ?? "Unknown workflow error.");
if (evt is WorkflowOutputEvent { Data: ReviewResult result }) results.Add(result);
Code source · Maf.Workflows · Dossier
- shared/Maf.Workflows/Maf.Workflows.csproj
- demos/Demo230_TypedWorkflow/Demo230.App.csproj
- demos/Demo240_AgentWorkflow/Demo240.App.csproj
- demos/Demo250_WorkflowEvents/Demo250.App.csproj
Demo260 crée BacklogAgent et KnowledgeAgent avec deux sessions distinctes, puis Route choisit un seul agent. Les préfixes français où/comment/pourquoi et les chaînes 401, 500, logs ou rétention vont à Knowledge ; le reste va à Backlog. Les sessions vivent en mémoire et ne partagent pas de store métier. Ce routage lexical C# est observable et limité : une question anglaise sans ces signaux peut aller à Backlog. Il ne s’agit ni d’un classificateur sémantique ni d’un graphe MAF.
static RouteTarget Route(string input)
{
var s = input.Trim().ToLowerInvariant();
if (s.StartsWith("où") || s.StartsWith("comment") || s.StartsWith("pourquoi"))
return RouteTarget.Knowledge;
if (s.Contains("401") || s.Contains("500") || s.Contains("logs") || s.Contains("rétention"))
return RouteTarget.Knowledge;
return RouteTarget.Backlog;
}
enum RouteTarget { Backlog, Knowledge }
Prédire puis vérifier une branche
dotnet run --project demos/Demo230_TypedWorkflow/Demo230.App.csproj -c Release --no-build -- --incompleteDepuis la distribution complète compilée, prédire AnalyzeDraft → ClarifyDraft, puis contrôler NeedsClarification et l’absence d’appel modèle. Répéter avec --invalid pour le composant Unknown. Dans les tests, ClarificationSkipsNativeAgentEntirely mesure zéro appel et NativeAgentRunsOnceWithSchemaAndHostPostValidation mesure un seul appel sur la branche prête, avec un transport scripté.
dotnet test tests/Maf.Workflows.Tests/Maf.Workflows.Tests.csproj -m:1 -c Release --no-build --no-restorePortée de ce graphe
Construire un graphe neuf par entrée indépendante. PrepareReview garde un snapshot dans un champ local non sérialisé : ce graphe n’est pas un service concurrent réutilisable ni un exemple de checkpoint durable. Les variantes natives utilisent le profil chat commun, sans embeddings, et ne basculent pas automatiquement vers une API payante. Les tests scriptés ne qualifient pas granite4:3b ni un autre fournisseur.
Du chemin au temps
Prérequis
Pour poursuivre
- Demo230 — Typed Workflow
- Demo240 — Agent Workflow
- Demo250 — Workflow Events
- Demo260 — Multi Agent Roles
Précédent
Suivant
