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
- Typed messages and branches
- Native agent in the graph
- Events, roles and terminal output
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.
| Mechanism | Scope |
|---|---|
| Ordinary C# | Fixed rules and explicit calls: Demo260. |
| Native workflow | Executors, messages and edges: Demo230–250. |
| JSON plan | Data 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();
Source code · Maf.Workflows · Folder
- 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
- Errors or questions → ClarifyDraft → NeedsClarification
- Valid → AcceptDraft → Ready (Demo230)
- 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();
Source code · Maf.Workflows · Folder
- 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);
Source code · Maf.Workflows · Folder
- 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 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."]);
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);
Source code · Maf.Workflows · Folder
- 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 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 }
Predict then verify a branch
dotnet run --project demos/Demo230_TypedWorkflow/Demo230.App.csproj -c Release --no-build -- --incompleteFrom 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-restoreScope 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
- Demo230 — Typed Workflow
- Demo240 — Agent Workflow
- Demo250 — Workflow Events
- Demo260 — Multi Agent Roles
Previous
Next
