Skip to content

FAQ

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.

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.

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.

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.

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.

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.

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.

Yes. Affiant’s public roadmap lists what’s being worked on now, next, and later — by status, never by date — at /roadmap/.