FAQ
Is Affiant Semantic Kernel–only?
Section titled “Is Affiant Semantic Kernel–only?”No. Affiant’s interception logic runs identically over three backends — Semantic Kernel (SK),
Microsoft Agent Framework (MAF), and Microsoft.Extensions.AI (M.E.AI) — each a thin translation
bridge (Affiant.SemanticKernel, Affiant.AgentFramework, Affiant.Extensions.AI) over one
shared, backend-neutral pipeline defined once in Affiant.Core. No bridge carries
provenance-tagging or review-gating logic of its own — both live in Affiant.Core only.
Inference is nearly as neutral: the trigger, merge and idempotency machinery
(InferenceTriggerFilter, TaskInferenceRunner, TaskInferenceStep) is defined once in Core,
but each bridge ships its own implementation of IInferenceCompletionPort — the interface
itself is declared once, in Affiant.Abstractions — and that implementation builds the
structured-output extraction prompt and makes the completion call with tool routing off. Only
the Semantic Kernel port completes against a backend-specific abstraction — SK’s
IChatCompletionService, handed a PromptExecutionSettings carrying
FunctionChoiceBehavior.None() to turn routing off. The MAF and M.E.AI ports both complete
against the same Microsoft.Extensions.AI abstraction, calling
IChatClient.GetResponseAsync(messages, options: null, ct) and deliberately passing no options
at all, so the request advertises no tools for a FunctionInvokingChatClient anywhere in the
chain to route to. See Interception Backends for how to
choose between them.
The provenance model itself has never been backend-specific. Affidavit, ProvenanceTag,
ProvenanceChain, ToolEnvelope and DocketEntry all live in Affiant.Abstractions, and so does
every backend-neutral contract a host implements — they describe data (what was written and where
it came from), not how a tool call happened to get invoked. Exactly three of the ten packages
declare a public interface of their own outside Abstractions, and each one is local to the package
that defines it rather than a contract a host implements: IManualToolInvoker in
Affiant.SemanticKernel, IAffiantWrappedFunction in Affiant.Extensions.AI, and
IAffiantHubClient in Affiant.Transport.SignalR. Affiant.AgentFramework declares none. See
Packages for the full dependency picture.
What about Microsoft Agent Framework (MAF) and Microsoft.Extensions.AI (M.E.AI)?
Section titled “What about Microsoft Agent Framework (MAF) and Microsoft.Extensions.AI (M.E.AI)?”Both are first-class backends as of 1.0.0-beta.1, not a roadmap item. SK was the original
backend; Affiant.AgentFramework joined the package set 2026-07-05, and Affiant.Extensions.AI
joined 2026-08-20. See
Using Affiant with Microsoft Agent Framework and
Interception Backends for the host-facing guides.
Microsoft’s Agent Framework team has said new feature investment goes to MAF while Semantic
Kernel receives critical-bug and security fixes, with support guaranteed for at least one year
past MAF’s general availability. MAF reached GA on 2026-04-03, so that is a floor of roughly
April 2027 — a floor, not a dated end-of-life. See the
SK-to-MAF migration guide
for Microsoft’s current framing of the two side by side. Nothing in Microsoft’s primary
documentation sets a hard SK end-of-support date. Picking MAF or M.E.AI over SK for a new host is
forward-looking, not required: provenance tagging, the Affidavit projection and the Review Gate are
the same Affiant.Core code whichever bridge you wire. What does turn on the choice is narrow.
Pre-tool inference deduplicates per turn on Semantic Kernel, which reads the turn number out of
kernel.Data["AffiantTurnNumber"], and per round of a turn on MAF and M.E.AI, which take it from
the invoking client’s iteration counter — the answer on the agent loop below spells that out. The
hosted-tool audit ships in the MAF and M.E.AI bridges only, as the next paragraph describes;
Semantic Kernel has no equivalent, though its own AffiantStartupValidator refuses two other
wirings at start-up — a [KernelFunction] the tool registry has never heard of, and a descriptor
naming an inference strategy the container cannot resolve. And the arguments the model passed reach
the entry-id material on MAF and M.E.AI but not on Semantic Kernel, where the gate runs at SK’s
auto-invocation seam and sees none — so a call carrying arguments derives a different Docket row id
there than it would on the other two.
All three backends draw the same hosted-tool line. FunctionInvocationContext — a
Microsoft.Extensions.AI type, shared by MAF’s function-invocation middleware and M.E.AI’s
FunctionInvokingChatClient rather than belonging to either, and exposing an arguments view and a
terminate flag — is documented as context “for an in-flight function invocation”, so it exists only
for the calls the client invokes itself; what decides is the loop around it, not the context object.
FunctionInvokingChatClient’s published remarks say that loop invokes the AIFunction a call names
in ChatOptions.Tools (or in its own AdditionalTools), and hands a declaration that is not an
AIFunction back to the caller uninvoked. A call the provider itself already executed is flagged
FunctionCallContent.InformationalOnly, whose documented meaning is that the function “has already
been processed … and should be ignored by components that process function calls”. Semantic Kernel
draws the same line. Hosted tools (hosted MCP, code interpreter, web search, and similar
provider-executed tools) bypass all three exactly the same way. Only hosted MCP carries an approval
mechanism of its own — HostedMcpServerTool.ApprovalMode, with never, always and require-specific
modes; HostedWebSearchTool and HostedCodeInterpreterTool expose no approval surface at all, so
for those two there is nothing to fall back on. Read
the honest boundary for the full shape of that limit. Two of the
three bridges reproduce it structurally: the Affiant.AgentFramework and Affiant.Extensions.AI
packages each ship a hosted-tool audit that refuses at wire-up by default rather than staying
silent about an uncovered hosted tool. Semantic Kernel ships no such audit — there the boundary is
documented, not enforced.
Where that comes from: the coverage boundary is recorded in the framework repository’s own MAF
adapter documentation (docs/adapters/microsoft-agent-framework.md, “The hosted-tool boundary”),
which states the limitation plainly, and in the adapter’s design record
(docs/proposals/affiant-maf-adapter.md), which is the document carrying the date — it states the
boundary as verified against Microsoft’s primary documentation on 2026-07-04. The same record
sources the maintenance-mode framing above to the Agent Framework team’s devblog post of
2025-10-07, Semantic Kernel and Microsoft Agent Framework, and the GA date to the Microsoft
Agent Framework Version 1.0 post.
Should I use PostgreSQL or SQLite?
Section titled “Should I use PostgreSQL or SQLite?”Both are first-class backends, picked per environment rather than one being the “real” option and
the other a toy. Affiant.EntityFramework ships PostgresDocketStore and SqliteDocketStore
alongside the matching PostgresChatSessionStore and SqliteChatSessionStore for session
persistence; Affiant.Docket ships InMemoryDocketStore and the expiry sweep. What the framework
requires of every host is only that some package registered an IDocketStore by the time the
application starts — that is the whole of the startup validator’s docket check. AddAffiantDocket()
is what a SQL-backed host adds on top of AddAffiantEntityFramework(...) for the shipped sweep: the
hosted service that moves lapsed-TTL entries to Expired, warns as a deadline approaches, and
re-broadcasts Evidence Cards still pending. A host that would rather drive that itself — a
serverless deployment with no long-lived process, a cron entry, a queue worker — omits the call
and invokes IDocketStore.ExpireDueAsync on its own cadence, which the framework sanctions
explicitly. See
Docket & Evidence Cards for exactly
which package registers what, and why the split exists.
Use SQLite for development and testing — zero external services to stand up, and fast to reset
between test runs. Use PostgreSQL for production — Postgres gives you the durability and
concurrent-write guarantees a review queue under real traffic needs, and three of the Docket’s JSON
columns — the proposed Affidavit, its provenance chains and a reviewer’s amendments — are stored
there as jsonb rather than text, which is what makes them GIN-indexable. The rest of the row’s
JSON, the amended-affidavit payloads included, is text on Postgres too. InMemoryDocketStore is
the process-local store for a host that wants no database at all —
AddAffiantDocket(d => d.UseInMemory()), and nothing else is needed. Nothing in it survives a
restart, which makes it a fit for tests and demos rather than for anything a reviewer has to come
back to.
What transfers between the two is behavior, not the schema. SqliteDocketStore and
PostgresDocketStore share one implementation of the store contract (EfDocketOperations)
verbatim, so the framework’s own queries behave identically on both. The tables do not match,
deliberately: AffiantDbContext applies the jsonb mappings only under Npgsql, so those Docket
payloads are the columns Affidavit, ProvenanceChains and Amendments on Postgres and the TEXT
columns AffidavitJson, ProvenanceChainsJson and AmendmentsJson on SQLite.
The provisioning call is shared; the schema path behind it is not. AffiantMigrator’s
MigrateAffiantSchemaAsync branches on one test — whether the configured provider name is
Microsoft.EntityFrameworkCore.Sqlite. On SQLite no migration runs at all: it calls
EnsureCreatedAsync and then heals known drift, adding the columns and indexes an
already-provisioned database is missing and backfilling three integer tick columns from the
instants they mirror — CreatedAtTicks, ExpiresAtTicks and, where it is still null,
DecidedAtTicks — on every row whose created-at or expires-at ticks are still zero. Every other
provider takes the else branch and runs Database.MigrateAsync over the checked-in migration
history, and Postgres is the provider that history was generated against. That is also why SQLite
cannot simply run it: the migrations were generated with Npgsql active, so running them against
SQLite would produce a table EF’s own SQLite-mapped model cannot query; a SQLite-native migration
history is tracked as a follow-up.
Is Affiant net10.0 only?
Section titled “Is Affiant net10.0 only?”Yes. All ten packages target net10.0 exclusively — there’s no net8.0 or netstandard2.0
multi-targeting today. If that’s a blocker for your host application, raise it on
the GitHub repository.
What license is Affiant under?
Section titled “What license is Affiant under?”Apache-2.0. It’s a deliberate choice for a framework that sits in the write path to a system of
record: Apache-2.0 carries an explicit patent grant, which matters to the enterprise legal teams
who scrutinize anything touching their production database, and it deters patent-based threats
against adopters. See the LICENSE and NOTICE files in
the repository for the full text.
Is Affiant production-ready?
Section titled “Is Affiant production-ready?”Affiant is in beta. The public API — Affidavit, ProvenanceTag, ToolEnvelope, the DI
extension methods, the package boundaries themselves — has been exercised end-to-end by two
independent first-party host applications — both publicly reachable:
Meridian and HR Portal — but it hasn’t reached 1.0 general availability yet,
and will keep changing before it does.
Adopt on this basis: trust the invariant, expect the API to evolve. The invariant —
Rule 7, every Affidavit field carries provenance, no exceptions
— is stable, and Affiant.Testing.ComplianceHarness exists specifically to enforce it as a CI
gate in your own project, not just in Affiant’s. Type shapes, DI signatures, and package
boundaries may still change between beta and 1.0 GA. Pin all ten packages to the same version and
read the changelog before upgrading.
Does Affiant slow down the agent loop?
Section titled “Does Affiant slow down the agent loop?”For read tools, no added LLM cost: context extraction runs in deterministic framework filters
(ContextExtractor subclasses, implementing Affiant.Abstractions.Interfaces.IToolInvocationFilter
and running on all three backends), not as an extra model call — that’s
Rule 4, filters over prompts for determinism.
For write tools decorated with [AffiantWriteTool], there is
one real cost: InferenceTriggerFilter runs a structured-output completion (through
IInferenceCompletionPort) before the tool executes, to fill in fields the
Context Fabric doesn’t already hold deterministically. The one
trigger the framework ships reads the operation the attribute declares and fires on WriteCreate
and WriteUpdate only, so a tool declared with any other operation string — WriteDelete, say —
is governed by the Review Gate like any other write tool and costs no round-trip at all. Where it
does fire, that’s one extra model round-trip on the write path — but it’s bounded and fails safe.
It runs at most once per (ConversationId, FunctionName, TurnNumber), and both identity values
come from the backend: Semantic Kernel reads them out of kernel.Data (ConversationId and
AffiantTurnNumber), so a host that counts its own turns gets one inference per write tool per
turn, while MAF and M.E.AI take the conversation id from the run’s ChatOptions.ConversationId
and the turn number from the invoking client’s iteration counter, which makes the deduplication
there per round of a turn rather than per turn. Where no conversation id reaches the filter at
all, it substitutes the fabric instance’s own hash — easier to hit on those two backends than it
looks, because under Microsoft.Extensions.AI 10.9.0 the FunctionInvokingChatClient in the
chain re-derives the in-flight options’ ConversationId from each provider response: against a
stateless provider, one whose responses carry no conversation id of their own, the host’s id reaches
the filter on every tool call of a turn’s first round and null does from the second round onward.
If the completion errors — a provider outage, or a response that isn’t the JSON the schema asked for
— TaskInferenceRunner catches everything except cancellation, emits an inference.failed
telemetry event tagged with affiant.error.kind, logs a warning, and returns an empty result, so
the tool call proceeds. That’s Rule 5, graceful degradation,
applied to the inference step specifically.
What “degraded” means concretely: the fields inference would have filled keep
ProvenanceTag.Empty, an Empty field scores 0, and the Affidavit’s aggregate confidence is
the minimum over its fields — so the number a reviewer sees drops. Nothing writes a warning
onto the Affidavit about it: Affidavit.Warnings carries the business-rule warnings the host
hands to the projection plus the three sentences the
Review Gate appends at filing — the reason a
Standing Order was held back, a tool the host declared uncovered, and a requirement level this
release records but does not run — and nothing in the inference path adds to it. The failure is
visible in telemetry and in the confidence score, not on the card as prose.
One case is not graceful, and it’s worth knowing before you rely on the fail-safe: nothing in the
inference path implements a timeout — not TaskInferenceRunner, not any of the three
IInferenceCompletionPort adapters, not AffiantCoreOptions. A provider or HTTP timeout arrives
as a TaskCanceledException, which is an OperationCanceledException, and cancellation is
exactly what the port, the runner and InferenceTriggerFilter all rethrow rather than absorb, so
it escapes the tool onion before the tool body runs. When it is the provider that timed out and
the host’s own token is still live, the invoking client is what sees it: Microsoft.Extensions.AI’s
FunctionInvokingChatClient records a failed tool call and lets its loop continue, so the model
answers without the tool. That tolerance has a documented ceiling —
MaximumConsecutiveErrorsPerRequest, three consecutive failing iterations by default, after which
the exception is rethrown to the caller instead. The write tool never executes and no proposal is
filed: the loop carries on and the person gets an answer, but the governed write is silently lost.
Bound the call from the host side — the cancellation token you pass in is what governs it.
Can I use Affiant without SignalR?
Section titled “Can I use Affiant without SignalR?”Yes. Affiant.Transport.SignalR is a reference implementation of IStreamingTransport, not a
requirement — the interface itself lives in Affiant.Abstractions, and everything above the
transport layer (the Review Gate, the Docket, the
policy graph) depends only on that interface, never on SignalR concretely.
To swap it out, implement IStreamingTransport yourself and register it in place of calling
AddAffiantSignalR<THub>(...). Three of the interface’s four members are required — SendAsync,
BroadcastToGroupAsync and AwaitEvidenceCardResponseAsync; the fourth, TryDeliverResponse, is
a default interface member returning false, so a transport that keeps no in-process waiter
registry doesn’t have to implement it at all. AwaitEvidenceCardResponseAsync is the one member
confined to the blocking filing path, ReviewGate.FileReviewAsync, marked [Obsolete]
(AFFIANT0002) as of 1.0.0-beta.3; TryDeliverResponse is what unblocks it, and
ReviewGate.HandleDecisionAsync calls that member on the live, non-blocking path too —
unconditionally after every approve and reject, once the row is already written and attested,
purely to notify a waiter if one happens to exist, and without branching on what it returns. See
Transport & Wire Contract for the full interface and what
each method is responsible for.
Is there a roadmap?
Section titled “Is there a roadmap?”Yes. Affiant’s public roadmap lists what’s being worked on now, next, and later — by status, never by date — at /roadmap/.