Using Affiant with Microsoft Agent Framework
Status: the Microsoft Agent Framework (MAF) bridge described on this page is merged to main
in the Sakwala/affiant repository — a first-class,
fully-tested part of the ten-package 1.0.0-beta.1 set, alongside Affiant.SemanticKernel and
Affiant.Extensions.AI (the third backend; see Interception Backends).
Everything below documents real, tested code, verified against the framework source at commit
055a2b1. See Packages for the full package set.
Meridian, the public demo at meridian.affiant.dev, runs on this adapter.
What MAF is
Section titled “What MAF is”Microsoft Agent Framework is Microsoft’s current-generation .NET agent SDK — the successor to Semantic Kernel, reaching general availability on 2026-04-03. Microsoft has said new feature investment goes to MAF while SK receives critical-bug and security fixes, with SK support guaranteed for at least one year past MAF’s GA (a floor, not a published end date). If you’re starting a new .NET agent project today, Microsoft steers you to MAF; if you already run SK, nothing forces a migration on any particular timeline. See the FAQ for the full sourcing on that timeline.
Affiant’s stance: SK remains a fully-supported, first-class backend. The MAF bridge is a forward hedge, not a replacement — both can run side by side, and nothing about adding MAF support changes what an SK host already has wired up.
The adapter’s shape
Section titled “The adapter’s shape”Affiant.AgentFramework is Affiant’s second interception backend, sitting beside
Affiant.SemanticKernel behind one shared, backend-neutral pipeline. The provenance tagging, task
inference, and review-gating behavior is identical either way — the same core filters run
regardless of backend. What differs is only how the framework attaches to your agent runtime:
- Neutral pipeline. All interception logic — provenance tagging, task inference, review gating — is defined once, backend-neutrally. Each backend package is a thin translation bridge over its framework’s native tool-interception seam; neither bridge contains any tagging, inference, or review-gate logic of its own.
WithAffiant(...)wrapping. MAF attaches middleware by decoration, not DI: it builds a new wrappedAIAgentrather than mutating the one you pass in.WithAffiant(serviceProvider, catalog)is the single blessed way to attach Affiant — it registers your tool descriptors, runs the hosted-tool audit (below), attaches the middleware, and returns the wrapped agent.AffiantToolCatalog. MAF has no[KernelFunction]-equivalent marker attribute, soAffiantToolCatalog.FromType<T>()reflects over every public instance method on your tool type in one pass, producing both theAIFunctions MAF invokes and the descriptors the pipeline reads. Practical consequence: keep a MAF tool type’s public surface limited to tool methods — an unrelated public helper becomes a callableAIFunctiontoo.- Sealing by return, not mutation. MAF’s middleware delegate has no settable
context.Resultthe way SK’s filters do — the delegate’s return value is the function result. Affiant’s bridge seals evidence by returning the (possibly replaced) value, and maps a filter’s termination request onto MAF’s own.Terminateflag, used sparingly because Microsoft documents it as able to skip sibling calls or leave chat history inconsistent.
A wiring example
Section titled “A wiring example”This mirrors the real wiring in Affiant.AgentFramework (verified against
src/Affiant.AgentFramework/Extensions/AgentExtensions.cs and
src/Affiant.AgentFramework/AffiantToolCatalog.cs in the framework repository):
using Affiant.AgentFramework;using Affiant.AgentFramework.Extensions;using Affiant.Core.Extensions;using Microsoft.Agents.AI;
// 1. Register the neutral pipeline and this backend's bridge.builder.Services.AddAffiantCore();builder.Services.AddAffiantAgentFramework();builder.Services.AddSingleton<IChatClient>(/* your provider's IChatClient */);builder.Services.AddScoped<WorkOrderTools>(); // your tool type
// 2. Build the underlying agent, then wrap it.var serviceProvider = builder.Services.BuildServiceProvider();var chatClient = serviceProvider.GetRequiredService<IChatClient>();var catalog = AffiantToolCatalog.FromType<WorkOrderTools>();
AIAgent agent = new ChatClientAgent( chatClient, instructions: "You are a work-order assistant.", tools: catalog.Functions.Cast<AITool>().ToList(), services: serviceProvider) .WithAffiant(serviceProvider, catalog);Wrapping produces a new AIAgent instance. MAF’s AsBuilder().Use(...).Build() decorates
rather than mutates the original agent — a pre-wrap agent that a host retains and calls instead of
the wrapped instance silently bypasses Affiant entirely. Discard the unwrapped local, or shadow
it, so nothing in your codebase can call the version with no provenance tracking:
// Wrong — the unwrapped `agent` variable is still callable and bypasses Affiant entirely.var agent = new ChatClientAgent(chatClient, instructions, tools: catalog.Functions.Cast<AITool>().ToList(), services: sp);var wrapped = agent.WithAffiant(sp, catalog);await agent.RunAsync(userMessage, session); // BUG: no provenance captured, no review gate
// Right — only the wrapped instance exists in scope past the wiring line.AIAgent agent = new ChatClientAgent(chatClient, instructions, tools: catalog.Functions.Cast<AITool>().ToList(), services: sp) .WithAffiant(sp, catalog);await agent.RunAsync(userMessage, session); // Affiant's middleware is in the call chainAddAffiantAgentFramework() is the MAF analog of SK’s AddAffiantSemanticKernel() +
AddAffiantInferenceOrchestration() combined into one call — MAF has a single function-calling
seam rather than SK’s invocation/auto-invocation split, so there’s no separate “pre-tool” and
“post-tool” registration to keep apart.
The hosted-tool boundary
Section titled “The hosted-tool boundary”Stated plainly: Affiant on MAF swears only to writes made by locally-invoked tools. This is the same boundary Affiant draws on SK — see The Honest Boundary for the full shape of it — reproduced one layer up the stack, not removed.
MAF’s function-calling middleware fires only for client/locally-invoked tools: function tools
(AIFunction) and local MCP tools. Hosted/provider-side tools bypass it entirely — hosted
MCP, code interpreter, web search, file search, and other server-executed toolboxes run on the
LLM provider’s own infrastructure and never enter the client middleware pipeline. There is no MAF
extension point that would let Affiant observe them. If your agent has a hosted tool that can
write anywhere, Affiant cannot see, tag, or gate that write — full stop.
WithAffiant(...) makes this structural rather than a silent gap, by auditing the agent’s tool
set before its first turn:
-
Default: refuse. If the wrapped agent’s tool set contains any tool that is not an
AIFunction,WithAffiantthrows, naming every uncovered tool. -
Override: explicit acknowledgment.
AgentFrameworkOptions.AcknowledgeUncoveredTools = ["code_interpreter", ...]permits named hosted tools to pass through. Each acknowledgment emits a telemetry span and a logged warning at wrap time, so it’s auditable, never silent:builder.Services.AddAffiantAgentFramework(options =>{options.AcknowledgeUncoveredTools = ["code_interpreter"];});
The rationale is the same one behind Affiant’s whole design: “Nothing commits without evidence. Nothing writes without approval.” A silently uncovered write path breaks that promise while the host believes it holds — refusal-by-default makes the boundary structural instead of a footnote.
Migrating an SK host to MAF
Section titled “Migrating an SK host to MAF”Moving an existing Affiant.SemanticKernel host to Affiant.AgentFramework — or running both
side by side, which nothing forbids — changes no provenance, inference, or review-gate behavior:
the neutral pipeline is identical either way. What changes at the call site:
| Concern | SK host | MAF host |
|---|---|---|
| Tool registration | [KernelFunction] + plugin registration |
Every public method reflected by AffiantToolCatalog.FromType<T>() — no marker attribute |
| Attach Affiant | DI-registered filters on the Kernel |
agent.WithAffiant(services, catalog) — a decoration producing a new AIAgent |
| DI setup | AddAffiantSemanticKernel() + AddAffiantInferenceOrchestration() |
AddAffiantAgentFramework() — one call |
| Provider abstraction | IChatCompletionService + connector capabilities |
Microsoft.Extensions.AI.IChatClient — MAF’s own provider abstraction |
| Session state | SK ChatHistory |
MAF AgentSession (agent.CreateSessionAsync()) |
| Hosted-tool coverage | Documented in prose only | Structural, enforced at WithAffiant time |
One naming gotcha worth flagging: SK’s plugin walker strips a bare trailing Async from a method
name when no explicit name is given; AffiantToolCatalog.FromType<T>() does not. A method named
CreateWidgetAsync becomes tool CreateWidgetAsync on MAF but tool CreateWidget on an
equivalent SK plugin. If you want identical tool names across an SK and a MAF host built from the
same domain type, avoid trailing Async in method names.
Where this fits
Section titled “Where this fits”The Honest Boundary covers the hosted-tool limit in full and states
why it’s architecturally true rather than a missing feature. Interception Backends
compares all three bridges side by side, including Affiant.Extensions.AI — the third backend —
and the ConversationId gotcha that affects task inference across all of them at this seam.
Packages has the full ten-package dependency graph. Authoring
Write Tools and Authoring Read Tools
cover the tool-authoring patterns this guide assumes; they’re framework-generic and apply
unchanged whether your host runs SK, MAF, or M.E.AI.