e-naxosMAF COURSE / COURS
Français

MICROSOFT AGENT FRAMEWORK · 0.16.0-rc.1

Typed graphs and events

Describe execution paths with typed messages and validate the graph’s business output.

Objectives

Who chooses the path?

A backlog request arrives with an unknown component. The program must request clarification before review. Video2 introduced this question through an older B2B scenario; the current baseline examines a host-supplied BacklogDraft. It neither classifies free text nor creates a backlog row. The graph makes possible paths explicit, and events reveal the path actually taken.

Choose an orchestration
MechanismScope
Ordinary C#Fixed rules and explicit calls: Demo260.
Native workflowExecutors, messages and edges: Demo230–250.
JSON planData proposed by the model: Demo270–290.

Contracts and exhaustive branches

Executor<TInput,TOutput> declares exchanged messages. AnalyzeDraft copies the questions array, calls Validate and produces DraftDecision. IsReady depends on host-computed errors. The two conditions cover both cases: an incomplete draft reaches ClarifyDraft, a valid one reaches AcceptDraft or PrepareReview. Clarification produces NeedsClarification as terminal output; it does not automatically restart a 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();

shared/Maf.Workflows/BacklogWorkflow.cs · L43–L52 · 9a895c94534c

Source code · Maf.Workflows · Folder

Possible draft paths
  1. BacklogDraft → AnalyzeDraft → DraftDecision
  2. Errors or questions → ClarifyDraft → NeedsClarification
  3. Valid → AcceptDraft → Ready (Demo230)
  4. Valid → PrepareReview → native agent → ValidateReview → Reviewed or Rejected

Insert a native agent and retain the boundary

In the linked MAF baseline, BindAsExecutor adapts a real ChatClientAgent to the graph. Earlier slides announcing this integration as future work are outdated for this baseline. PrepareReview sends ChatMessage then TurnToken, with two SendsMessage declarations. The native host buffers the first message; the token triggers the turn. Two deliveries to one executor can therefore represent a single model call.

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();

shared/Maf.Workflows/BacklogWorkflow.cs · L57–L66 · 9a895c94534c

Source code · Maf.Workflows · Folder

[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);

shared/Maf.Workflows/BacklogWorkflow.cs · L110–L121 · 9a895c94534c

Source code · Maf.Workflows · Folder

ForwardIncomingMessages=false prevents user JSON from becoming generated downstream output. ValidateReview retains Assistant messages only, then checks JSON, domains and equality of every fact against the host snapshot. A changed priority is rejected even when the JSON is valid. Reviewed means neither human approval nor a write. This strict round trip teaches the boundary; a real application needs no model to copy a valid contract.

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."]);

shared/Maf.Workflows/BacklogWorkflow.cs · L70–L82 · 9a895c94534c

Source code · Maf.Workflows · Folder

Events, roles and evidence

Demo250 consumes all of WatchStreamAsync. ExecutorInvokedEvent describes actual deliveries; AgentResponseEvent exposes the raw response; only WorkflowOutputEvent carrying ReviewResult supplies business results. Native Activity spans enabled by WithOpenTelemetry measure causality and duration without an external collector. Neither an event nor a span establishes a stored row. Workflow event streaming remains active while agent token updates are disabled.

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);

shared/Maf.Workflows/WorkflowDemo.cs · L46–L52 · 9a895c94534c

Source code · Maf.Workflows · Folder

Demo260 creates BacklogAgent and KnowledgeAgent with two separate sessions, then Route selects one agent. French prefixes où/comment/pourquoi and strings 401, 500, logs or rétention select Knowledge; everything else selects Backlog. Sessions live in memory and share no business store. This lexical C# routing is observable and limited: an English question without these signals may go to Backlog. It is neither a semantic classifier nor a MAF graph.

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 }

demos/Demo260_MultiAgentRoles/Program.cs · L45–L55 · 9a895c94534c

Source code · Demo260 · Folder

Predict then verify a branch

dotnet run --project demos/Demo230_TypedWorkflow/Demo230.App.csproj -c Release --no-build -- --incomplete

From the complete built distribution, predict AnalyzeDraft → ClarifyDraft, then check NeedsClarification and no model call. Repeat with --invalid for component Unknown. In tests, ClarificationSkipsNativeAgentEntirely measures zero calls and NativeAgentRunsOnceWithSchemaAndHostPostValidation measures one call on the ready branch, using a scripted transport.

dotnet test tests/Maf.Workflows.Tests/Maf.Workflows.Tests.csproj -m:1 -c Release --no-build --no-restore

Scope of this graph

Build a fresh graph for each independent input. PrepareReview keeps a snapshot in a local field that is not serialized: this graph is neither a reusable concurrent service nor a durable-checkpoint example. Native variants use the common chat profile without embeddings and never automatically switch to a paid API. Scripted tests qualify neither granite4:3b nor another provider.

From paths to time

Prerequisites

Continue reading

Previous

Next

lot009-workflows-1 · 2026-10-24 · 9a895c94534cd5fc6f3da883e37e5f7856e588c1