Extend
Use any Microsoft Agent Framework feature
Does Tracon block a Microsoft Agent Framework (MAF) feature that it does not model? No. Every MAF feature runs under Tracon when your code builds the agent. The real question is what the agent loses on the way. This page puts the ways in order, lightest first, and says what each one keeps and loses.
Every code block on this page comes from samples/Tracon.Samples.MafFeatures. That
sample builds against the packed packages. Its test project checks six claims: the
decorator sees the tool calls of every door, the custom source uses the tenant’s own
key, the custom source records cost, the egress policy reaches the custom source and
not the factory agent, an agent extension adds a context provider to an agent defined
as data without losing the tenant’s key or cost, and a chat client ring that an extension
adds sees every model call of that agent.
The coverage ladder
Section titled “The coverage ladder”Four levels say how far a feature reaches into Tracon:
| Level | Meaning |
|---|---|
| Unavailable | You cannot use the feature, or using it turns off the run record or the tenant boundary. |
| Runs | You use the feature from code. The run of the agent is recorded. Some Tracon rings can be lost. |
| Recorded | The events, usage, and cost of the feature enter the run record and the span. The tenant boundary applies. |
| Managed | You turn the feature on with data: a definition, the HTTP API, or the console. Tracon validates it, versions it, and puts it under roles and the audit trail. |
The rule is: code defines, data selects. Anything that executes is registered in code — a tool, a context provider, middleware. A definition picks it by name. A feature that you wire in from code reaches Runs at once. It does not reach Managed by itself.
Pick the lightest door
Section titled “Pick the lightest door”Start at the top of the diagram and stop at the first door that fits.
flowchart TD
accTitle: Which door reaches a framework feature
accDescr: A decision tree. A workflow graph goes to a code workflow. A Microsoft.Extensions.AI type that Tracon reads from the container is registered in the container. Agent middleware, tool middleware, or a message context provider goes in an agent decorator. A framework option, a full context provider, or a chat client ring on an agent that a definition describes goes in an agent extension. Anything else needs an agent that code builds. If you need the tenant key, the egress policy, or cost, use a custom agent source. If you do not, use a factory agent.
START["A framework feature you want to use"] --> GRAPH{"Is it a workflow graph?"}
GRAPH -->|yes| WF["Code workflow<br/>AddWorkflow"]
GRAPH -->|no| DI{"Does Tracon read the type<br/>from the container?"}
DI -->|yes| REG["Register it<br/>in the container"]
DI -->|no| MW{"Is it agent middleware, tool middleware,<br/>or a message context provider?"}
MW -->|yes| DEC["Agent decorator"]
MW -->|no| OPT{"Is it a framework option, a full context provider,<br/>or a chat client ring, on an agent that a definition describes?"}
OPT -->|yes| EXT["Agent extension"]
OPT -->|no| KEY{"Do you need the tenant key,<br/>the egress policy, or cost?"}
KEY -->|yes| SRC["Custom agent source"]
KEY -->|no| FAC["Factory agent"]
| Door | Use it when | What you keep | What you lose |
|---|---|---|---|
| Container registration | Tracon reads the type from the container | Everything | Nothing |
| Agent decorator | The feature is agent middleware, tool middleware, or a message context provider | Everything: the model rings, the tenant key, the egress policy, cost | A full context provider |
| Agent extension | The feature is a framework option, a full context provider, or a chat client ring, and a definition describes the agent | Everything a decorator keeps, plus the definition: console editing, versions, validation | The options the definition owns are protected. A tool that your provider adds skips the tool chain |
| Factory agent | Code builds the agent and you need none of the three below | The run record, the model rings (through the registry), the wrapped tools | The tenant key, the egress policy, cost |
| Custom agent source | Code builds the agent and you need the tenant key, the egress policy, or cost | All of the above, plus the tenant key, the egress policy, and cost | Console editing and the features the compiler wires |
| Code workflow | The feature is a graph | The graph, in full | Events that Tracon does not map |
Register it in the container
Section titled “Register it in the container”Some Microsoft.Extensions.AI types need no agent at all. You register the type, and
Tracon uses it where it needs one. An IEmbeddingGenerator serves knowledge search, an
ITextToSpeechClient or ISpeechToTextClient serves voice, and an IEvaluator serves
evaluation. Register the type with the extension of its own package, then bind it as the
page of the feature says. Each page also says what the feature records:
- Knowledge and RAG — the embedding generator
- Voice and live conversation — speech clients
- Evaluation and experiments — evaluators
Wrap the agent with a decorator
Section titled “Wrap the agent with a decorator”An IAgentDecorator wraps every agent that the catalog resolves. A decorator can add
MAF middleware with AIAgentBuilder. It can add run middleware, function-invocation
middleware that sees each tool call, or a MessageAIContextProvider. The decorator
wraps the agent that the catalog already built, so the agent keeps every Tracon ring.
public sealed class ToolCallLogDecorator(ToolCallLog log) : IAgentDecorator{ public int Order => 100;
public AIAgent Decorate(AIAgent agent, AgentDescriptor descriptor) => agent.AsBuilder() .Use(async (innerAgent, context, next, cancellationToken) => { log.Record(descriptor.Name, context.Function.Name); return await next(context, cancellationToken).ConfigureAwait(false); }) .Build();}Give the decorator an Order above 0. Run recording sits at 0 and hands the agents
inside it a plain AgentRunOptions. MAF function middleware accepts that type. A
decorator outside run recording receives Tracon’s own run options, and the middleware
rejects them.
The decorator sees every agent. To act on one agent only, check descriptor.Name. The
sample tests run this decorator against three agents: one defined as data, one from a
factory, and one from a custom source. It sees the tool call of all three.
A decorator cannot add a full context provider, one that adds tools or instructions.
UseAIContextProviders takes only message providers. For a full provider on an agent that
a definition describes, use an agent extension. For an agent that code builds, use one of
the doors after it.
The sample registers the decorator together with the log it writes to:
builder.Services.AddSingleton<ToolCallLog>();
return builder.AddAgentDecorator<ToolCallLogDecorator>();See Write your own agent decorator for the full contract.
Add an option, a provider, or a ring with an agent extension
Section titled “Add an option, a provider, or a ring with an agent extension”An agent extension is code that you register once and a definition selects by name. It
reaches the options that Tracon builds the agent from, so it can set a framework option or
add a full AIContextProvider. It can also put a chat client ring into the model pipeline
of the agent. The agent still goes through the definition compiler. It keeps the model
rings, the tenant’s own key, the egress policy, and cost, and the console can still edit and
version it.
The sample adds a provider that puts a policy notice into the instructions of every model call:
return builder.AddAgentExtension( ExtensionName, context => { // Add after the providers Tracon built; the compiler refuses a change to them. var options = context.ChatAgentOptions!; options.AIContextProviders = [.. options.AIContextProviders ?? [], new PolicyNoticeProvider()]; }, description: "Adds the refund policy to the instructions of every model call.");private sealed class PolicyNoticeProvider : AIContextProvider{ protected override ValueTask<AIContext> ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default) => new(new AIContext { Instructions = Notice });}The sample puts the name in the definition of the agent. A stored definition does the same
with extensionNames, and the console offers the registered names in the agent editor:
ExtensionNames = [PolicyNoticeExtensionRegistration.ExtensionName],Add a chat client ring
Section titled “Add a chat client ring”context.UseChatClient puts a Microsoft.Extensions.AI chat client into one of two fixed
places in the model pipeline that Tracon builds. A client that the framework documents as
one that must see every model call belongs here: a check on each call, a log of each call,
or a reducer of the request. The sample logs every model call:
return builder.AddAgentExtension( ExtensionName, context => { var log = context.Services.GetRequiredService<ModelCallLog>(); var agent = context.Definition.Name;
context.UseChatClient(ChatClientSlot.ModelCall, inner => new ModelCallLogChatClient(inner, log, agent)); }, description: "Logs the role of the last message of every model call.");The ring derives from DelegatingChatClient. The sample’s ring records the role of the last
message and passes the call on:
public override Task<ChatResponse> GetResponseAsync( IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default){ var request = messages as IReadOnlyList<ChatMessage> ?? [.. messages]; Record(request);
return base.GetResponseAsync(request, options, cancellationToken);}The sample test runs an agent that calls a tool. The ring sees two calls: the question, and the call that takes the tool result to the model.
The two slots, from the outside of the pipeline in:
flowchart TB
accTitle: Where a chat client ring sits
accDescr: The model pipeline of a compiled agent from the outside in. Error normalization, finish-reason detection, the fallback chain, the circuit breaker, and attachment resolution are outermost. The Turn slot is next, above the tool-call loop. The ModelCall slot is right below the loop. The run budget, the response cache, hosted tool usage, the chat span, the content guard, and the concurrency limit are below it, and the provider's raw client is innermost.
N["Error normalization · finish reason"] --> FB["Fallback chain · circuit breaker"]
FB --> A["Attachment resolution"]
A --> T[["Turn slot"]]
T --> L["Tool-call loop"]
L --> M[["ModelCall slot"]]
M --> B["Run budget · response cache · hosted tool usage"]
B --> O["chat span"]
O --> G["Content guard · concurrency limit"]
G --> R["Provider client"]
| Slot | What the ring sees | Use it for |
|---|---|---|
ChatClientSlot.ModelCall |
Every model call of a turn, including the call that takes a tool result to the model | A check or a log of each call, a change to each request |
ChatClientSlot.Turn |
One call for each agent turn. The tool-call loop runs inside it | A reducer of the conversation, a log of each turn |
What follows from the order:
- The content guard inspects the messages that the ring sends. A user, assistant, or
tool message that the ring adds reaches the guard before the model. The guard does not
inspect a system message or the instructions in
ChatOptions, on any agent. Do not let a ring put text that it did not write there, for example a retrieved document. The ring also receives the answer after the guard has checked it. - The budget counts the calls that the ring sends. A ring that answers by itself and
does not call the client below it spends nothing and leaves no
chatspan. - The response cache keys on the request after the ring. Two requests that the ring makes the same share one cache entry.
- The fallback chain gets the same ring. When a fallback model answers in place of the agent’s model, the ring factory builds a ring for that model’s pipeline too.
- A provider failure passes up through the ring. The circuit breaker counts it, the
fallback chain moves on, and normalization masks it as before. An exception that the ring
throws by itself is your code’s: it is not counted and not masked, the same as an exception
from a wrapper that
context.Useadds. - The compaction summarizer and the judge of a loop criterion get no ring. They serve their own call, not the agent’s turn.
The ring factory runs each time the pipeline is built: when the agent is compiled, and when
a fallback model is first used. Keep it cheap and keep state that belongs to one run out of
it. With several rings in one slot, the first one added is the innermost. Derive the ring
from DelegatingChatClient. A Turn ring that hides the tool-call loop from GetService
fails the compilation, because the framework would add a second loop above it. A host that
replaces IModelProviderRegistry with its own cannot place a ring: an agent that selects a
ring extension fails to compile on that host, with an error that names the extension.
What to know about this door:
Configureruns when the agent is compiled, not on every run. The compiled agent is cached for each tenant, agent name, and definition, so the runs that follow share it. Two runs that start before the cache holds the agent can each compile it. A run with parameter values, and an agent of a tenant that holds its own provider key, is not cached and runs the extension each time.- The extension gets one options object.
ChatAgentOptionsis set for a chat agent andHarnessAgentOptionsfor a harness agent.context.Useadds a wrapper around the compiled agent, inside Tracon’s own wrappers and decorators.context.UseChatClientadds a ring to the model pipeline of both kinds of agent. - Some options are protected. A definition or a tenant and security decision owns the
tool chain, the tool approval settings, the instructions and sampling settings (including the seed,
top-k, stop sequences, penalties, tool mode, and
AllowMultipleToolCalls), the chat history provider, file access, the working-directory skill scan, file memory, web search, compaction, the loop, and the sub-agent tree. Some other options are protected because they change a protected setting indirectly.UseProvidedChatClientAsIsremoves the framework’s default chat client stack, and with it the approval binding and the per-call history writer.AllowConcurrentInvocationturns on concurrent tool calls in the tool loop that Tracon builds. For concurrent tool calls, setToolLoop.AllowConcurrentToolCallsin the model binding of the definition.ChatOptions.RawRepresentationFactoryreplaces the model, sampling, and provider settings of the definition on the wire. Two options are protected because they do not work under Tracon.EnableMessageInjectionandRequirePerServiceCallChatHistoryPersistenceact on each model call only when they are under the tool loop. Tracon’s tool loop is inside the model client, so these layers are above it. An injected message does not reach the running turn, and the history is written once per turn. A layer of this kind belongs in theModelCallslot of a chat client ring. An extension that changes a protected option fails the compilation with an error that names the extension and the option. Tracon does not undo the change. The provider list is append-only. The other options are yours, for exampleThrowOnChatHistoryProviderConflict. The check covers the options object. It does not look inside a provider or a wrapper that the extension adds, so a provider you add can still read files or load skills by itself. Register only extension code that you trust, as you do for a tool. - A provider can add tools, and Tracon does not run them through the tool chain. Authorization, approval, and the timeout apply to the tools that the tool registry gives an agent. A tool that your own provider adds skips them. The run record still shows the call.
- A replay that plays tools back does not run the extension, for the same reason.
- Model options for one run follow the same rule. Code can run a catalog agent with
ChatClientAgentRunOptions. The run may add instructions and tool declarations, and it may turn parallel tool calls off (AllowMultipleToolCalls = false), which a handoff step does. A model id, a sampling setting, a tool mode, a response format, a chat client factory, or a tool that runs throwsArgumentExceptionbefore the run starts. No HTTP endpoint takes model options for one run. To change a run, change the definition, run an experiment, or declare a parameter.
Build the agent in a factory
Section titled “Build the agent in a factory”AddAgent(name, factory) takes a factory that returns any MAF AIAgent. Tracon records
the run and applies its decorators. The agent skips the definition compiler, so the
factory has to ask for each piece it wants to keep:
// The registry builds the model rings: run budget, circuit breaker,// fallback, response cache, content guards, and the 'chat' span. This// synchronous call applies no tenant key and no egress policy.var chatClient = services.GetRequiredService<IModelProviderRegistry>().CreateChatClient(model);
// The registry returns the wrapped tool: authorization, validation,// timeout, truncation, and approval stay on.if (!services.GetRequiredService<IToolRegistry>().TryGet(SearchOrdersToolRegistration.ToolName, out var searchOrders)){ throw new InvalidOperationException($"The tool '{SearchOrdersToolRegistration.ToolName}' is not registered.");}
return chatClient.AsAIAgent(new ChatClientAgentOptions{ Name = AgentName, ChatOptions = new ChatOptions { Instructions = "Answer questions about orders. Look orders up with the search tool.", Tools = [searchOrders], },
// Tracon's history store, so the console and /v1/conversations show the // turns. With SQL storage, leaving this out leaves both of them empty. ChatHistoryProvider = services.GetService<ChatHistoryProvider>(),
// Any framework provider or option is now yours. AIContextProviders = [new TodoProvider(new TodoProviderOptions())],});What a factory agent keeps, and how:
| Piece | Kept? | How |
|---|---|---|
| Run row, events, tool-call record, usage, cancellation, webhooks, error class, input record, replay | Yes | Automatic. Run recording wraps every agent. |
invoke_agent span and saved approval rules |
Yes | Automatic. |
Sessions, /v1/*, A2A and MCP publication, being called as a sub-agent |
Yes | Automatic. |
| The model rings | Yes | Build the chat client with IModelProviderRegistry.CreateChatClient. |
| The tool chain: authorization, validation, timeout, truncation, approval flag | Yes | Take the tool from IToolRegistry.TryGet. |
History in the console and in GET /v1/conversations/{id}/items |
Yes | Pass the ChatHistoryProvider from the container. |
| The tenant’s own provider key and the egress policy | No | The factory is synchronous. Use a custom agent source. |
| Cost, model breakdown, and cost quota | No | The descriptor of a factory agent names no model. Use a custom agent source. |
Three facts about this door:
- The history provider matters on SQL storage. With the in-memory store, the turn
shows in the console and in
/v1/conversations/{id}/itemswhether or not the agent uses the provider from the container. With a SQL store, both stay empty unless the agent uses it. The sample passes it. - The factory runs on every resolve. Keep it cheap, and keep state out of it.
- Tracon warns about a bypassed registry. If the chat client of a factory agent did
not come from
IModelProviderRegistry, Tracon writes one warning for that agent name the first time the catalog resolves it. An agent that exposes no chat client gets no warning.
Build the agent in a custom source
Section titled “Build the agent in a custom source”A factory is synchronous, and its descriptor names no model. An IAgentSource has
neither limit. Its ResolveAsync is asynchronous, so it can call
CreateChatClientAsync, which applies the tenant’s own provider key and egress policy.
Its ListAsync returns a descriptor that names the model, and run recording prices the
run from that model. The sample describes its agent like this:
// The model named here is what run recording prices. Leave it out and the// run is recorded with no model and no cost.private AgentDescriptor Descriptor => new(){ Name = AgentName, Description = "A custom-source agent that answers questions about invoices.", Origin = AgentDefinitionOrigin.Custom, SourceName = SourceName, Model = model, ToolNames = [SearchOrdersToolRegistration.ToolName],};ResolveAsync then builds the agent:
// The asynchronous call applies the tenant's own provider key and the tenant's// egress policy, on top of the model rings.var chatClient = await models.CreateChatClientAsync(model, cancellationToken).ConfigureAwait(false);
if (!tools.TryGet(SearchOrdersToolRegistration.ToolName, out var searchOrders)){ throw new InvalidOperationException($"The tool '{SearchOrdersToolRegistration.ToolName}' is not registered.");}
return chatClient.AsAIAgent(new ChatClientAgentOptions{ Name = AgentName, ChatOptions = new ChatOptions { Instructions = "Answer questions about invoices. Look orders up with the search tool.", Tools = [searchOrders], }, ChatHistoryProvider = history,});The sample tests run this agent for a tenant that has its own key. The chat client is built with that key, and the run record carries the model and a cost. The same tests run the factory agent for the same tenant: its client is built with the setup key, and its run record has no model and no cost. They also set an egress policy that forbids the provider. The custom source is refused. The factory agent still runs.
A custom source does not run the definition compiler. It owns the whole shape of the agent. Do not cache an agent whose client carries a tenant’s own key across tenants. Write your own agent source explains the lifecycle, the priority, and the contract tests.
Build the workflow in code
Section titled “Build the workflow in code”AddWorkflow(name, factory) runs a MAF WorkflowBuilder graph. Every builder feature
runs, and Tracon records a root run. Tracon maps the started, output, error, warning,
super-step, executor, request, and agent-response-update events into the run record. It
records every other event — the plan and ledger events of a Magentic workflow, or an event
class that you define — as a tracon.workflow.event with its type name and its data as
text (Runs and recording). Such an event has no card of its own in the
console.
If you write executors with MAF source-generated handlers ([MessageHandler]), add the
Microsoft.Agents.AI.Workflows.Generators analyzer package to your own project.
Tracon does not reference it for you.
See Workflows for the registration key, the helpers that give a code graph the time limit, retry, and budget of a stored one, and the full recording model.
Ready-made pieces from the framework
Section titled “Ready-made pieces from the framework”Some framework packages ship a finished piece instead of a hook. Three of them were tried under Tracon on 2026-10-21 with Microsoft Agent Framework 1.25. The shell tool and the remote agent ran against a real host; Purview was read from its package, because it needs a Microsoft 365 tenant.
- A shell tool in a container (
Microsoft.Agents.AI.Tools.Shell, preview).DockerShellExecutor.AsAIFunction()returns an ordinary tool. Register it withAddTooland name it in the definition: authorization, approval, the timeout, and the audit trail apply. UseShellMode.Stateless, so every call runs in a new container and no tenant or conversation sees another’s files. The persistent mode keeps one shell for one conversation, and a registered tool serves every run, so do not register a persistent executor. The executor starts thedockercommand line on the Tracon host, and access to a Docker socket is root access on that machine: point it at an engine that runs nothing else. The executor asks for approval by default, and Tracon reports that tool as one that needs approval. Do not useLocalShellExecutoron a shared host: it runs the command on the host itself. - A remote agent (
Microsoft.Agents.AI.A2A, preview).A2AClient.AsAIAgent()returns an agent that sends each turn to another A2A server. Register it withAddAgent(name, factory). The run is recorded at the agent level, and the session keeps the remotecontextId, so the next turn on the same Tracon session continues the remote conversation. The remote server runs its own model and tools: the record has no model, usage, or cost, and the remote run is not a child of the run that called it. The HTTP client that you give the agent does not go through Tracon’s egress policy. - Microsoft Purview (
Microsoft.Agents.AI.Purview, release candidate).PurviewExtensions.PurviewAgentMiddleware(...)returns agent middleware: apply it with an agent extension or a decorator. Build it once at startup, because each call builds its own cache, background queue, and HTTP client. Purview checks the input and the final answer of a run. When it blocks, it returns its message as an ordinary answer, so the run closesCompleted, not as blocked content. Its check of every model call is a chat client and needs a seat below the tool loop: theModelCallslot of a chat client ring. That form was not tried under Tracon. Its calls to Microsoft Graph do not go through Tracon’s egress policy.
What you cannot win back today
Section titled “What you cannot win back today”These losses have no door that removes them:
- A protected option on an agent defined as data. An agent extension can set any framework option that the definition and the tenant do not own. The tool chain, the chat history provider, the approval settings, the file access and skill-scan options, and the model settings stay with Tracon as options. A feature that needs one of them needs an agent that your code builds. That agent is code, so the console cannot edit it.
- The tool chain for a tool that your own provider adds. An extension’s provider can add a tool, and that tool skips authorization, approval, and the timeout. Register the tool in the tool registry instead, and name it in the definition.
- Tracon’s managed skills, compaction, MCP resources, and loop recording on a code-built agent. The definition compiler wires skills with their script gates, context compaction with its record, MCP tools and resources, loop recording, and the budget of the agent tree. It does this only when it compiles a definition. A factory agent or a custom-source agent gets none of them from Tracon.
- A file store that is scoped to the tenant and the agent. The agent file store in the container is not built for a provider of your own. The in-memory default has no tenant prefix and no agent prefix: every tenant and agent of the process shares it. The SQL stores scope every call to the tenant and to the agent of the current run, so a provider of your own shares the space of that agent’s file memory, and it fails outside a run. Do not give the in-memory store to an agent that serves more than one tenant. For files that belong to one conversation, use the session workspace instead: it is scoped to the tenant, the agent and the session, its file tools run through the tool chain, and its files go with the session.
- Everything a definition gives. A code-built agent cannot take parameterized, per-culture, or shared instructions. It has no versions, experiments, rollback, or validation when you save, and you cannot edit it in the console.
- The tenant key, the egress policy, and cost on a factory agent. A custom agent source gets all three.
Checklist
Section titled “Checklist”Answer these five questions before you write a factory agent:
- Can a decorator or an agent extension do the job? If the feature is middleware, a message provider, a framework option, a full context provider, or a chat client ring, one of them can, and you keep every ring.
- Do you need the tenant’s own key, the egress policy, or cost? If yes, use a custom agent source.
- Does the factory build its chat client with
IModelProviderRegistry? If not, the model rings are off, and Tracon logs a warning. - Does the agent take its tools from
IToolRegistryand its history from the container? If not, tool policy is off, and the console may show an empty history. - Can the agent live as code? Without console editing, versions, and rollback, and without Tracon’s skills, compaction, and MCP resources, is it still the agent you want?
Read next
Section titled “Read next”- Write your own agent decorator — the decorator door in full, with ordering rules
- Write your own agent source — the source contract, priority, and tenancy
- Agents and definitions — where the catalog gets its agents