# Write your own agent decorator

> Tracon documentation. Tracon is a .NET package family that adds a control
> plane on Microsoft Agent Framework, published as NuGet packages that run
> inside your own ASP.NET Core application.
>
> Page: https://tracon.dev/guides/write-your-own-agent-decorator/
> Summary: Wrap every resolved agent with custom cross-cutting behavior via IAgentDecorator.

`IAgentDecorator` wraps every agent the catalog resolves, the same mechanism
Tracon uses for run recording, telemetry, and the tool-approval gate. Use
it for cross-cutting behavior that has to see every agent — an audit trail,
a per-tenant rate limiter, a custom authorization check — rather than
repeating that logic inside each agent definition.

```csharp
public sealed class AuditingAgentDecorator(IAuditLog auditLog) : IAgentDecorator
{
    public int Order => 5;

    public AIAgent Decorate(AIAgent agent, AgentDescriptor descriptor)
        => new AuditingAgent(agent, descriptor.Name, auditLog);
}
```

`AddAgentDecorator` has three overloads, the same shape as `AddAgentSource` and
`AddRunJudge`. The generic one resolves the decorator's own constructor
dependencies through DI:

```csharp
builder.AddTracon()
       .AddAgentDecorator<AuditingAgentDecorator>();
```

Pass a ready instance, or a factory, when the decorator needs something that
does not belong in the container as its own singleton:

```csharp
builder.AddTracon()
       .AddAgentDecorator(sp => new AuditingAgentDecorator(sp.GetRequiredService<IAuditLog>()));
```

All three register the decorator as a **singleton** in the same
`IEnumerable<IAgentDecorator>` the catalog resolves; calling the generic
overload twice for the same type registers it once.

## Where it wraps

`Order` decides the position: the catalog sorts decorators by **descending**
`Order` and applies them in that sequence, so the **highest** value runs
first and ends up **innermost** — closest to the agent's own calls. Tracon's
own decorators use `0` (run recording, outermost), `10` (telemetry), `20`
(tool approval), and `30` (structured-response validation, innermost — closest
to the model call). Place a decorator that has to observe or veto everything
below it at a low value; place one that has to sit close to the model call
itself at a high value.

A decorator can also add Microsoft Agent Framework middleware with
`AIAgentBuilder`: run middleware, `UseAIContextProviders`, or
function-invocation middleware that sees each tool call. The agent keeps every
Tracon ring, because the decorator wraps the compiled agent.

```mermaid
flowchart LR
    accTitle: Where a decorator wraps
    accDescr: A run passes from the caller through run recording at Order 0, telemetry at 10, tool approval at 20 and structured-response validation at 30, then through a decorator with a higher Order, to the compiled agent. Run recording receives TraconRunOptions and passes a plain AgentRunOptions inward.
    CALLER["Caller<br/>TraconRunOptions"] --> REC["Run recording<br/>Order 0"]
    REC -->|"plain AgentRunOptions"| TEL["Telemetry<br/>Order 10"]
    TEL --> APP["Tool approval<br/>Order 20"]
    APP --> VAL["Structured response<br/>Order 30"]
    VAL --> YOURS["Your decorator<br/>Order 100"]
    YOURS --> AGENT["Compiled agent"]
```

```csharp
public sealed class ToolCallLogDecorator(ILogger<ToolCallLogDecorator> logger) : IAgentDecorator
{
    public int Order => 100;

    public AIAgent Decorate(AIAgent agent, AgentDescriptor descriptor)
        => agent.AsBuilder()
            .Use(async (innerAgent, context, next, cancellationToken) =>
            {
                logger.LogInformation("{Agent} calls {Tool}", descriptor.Name, context.Function.Name);
                return await next(context, cancellationToken);
            })
            .Build();
}
```

Give such a decorator an `Order` above `0`. Run recording (`Order` 0) hands the
agents inside it a plain `AgentRunOptions`. A decorator outside it receives
`TraconRunOptions`, and MAF's function-invocation middleware rejects that type
with `NotSupportedException`.

A decorator must be thread-safe — the catalog can decorate agents
concurrently — and must not swallow a genuine `OperationCanceledException` (one
thrown while the caller's token is cancelled). If `Decorate` throws anything else,
including an `OperationCanceledException` while the caller has not cancelled, the
catalog normalizes it into a
`TraconAgentSourceException` (the same shape a broken `IAgentSource`
produces) rather than letting a raw, unclassified error reach the caller.

A parameterized run compiled **outside** the catalog (bypassing
`CompiledAgentCache`) still needs the same pipeline; call
`AgentDecoratorPipeline.Apply(agent, descriptor, decorators)` by hand rather
than applying decorators one at a time — otherwise the agent runs
undecorated, with no run recording, no telemetry, and no tool-approval gate.

## How registration order decides who wins

`ITraconBuilder.Services` is the escape hatch for anything Tracon does
not model directly. Every Tracon service is registered with `TryAdd`, so
registration **order** decides who wins — and the answer is different
depending on whether the extension point allows one implementation or many.

| Seam shape | Examples | Registered **before** `AddTracon()` | Registered **after** `AddTracon()` |
|---|---|---|---|
| Single-instance | `ITenantContext`, `IProviderRetryClassifier`, `IRunErrorClassifier` | Your registration wins outright; only one registration remains | Also wins for a direct resolve, but Tracon's own registration is not removed — it stays behind as a second, unused entry |
| Multi-registration | `IAgentDecorator`, `IAgentSource`, `IRunJudge`, `IContentGuard`, `IModelProvider` | Your registration joins the list alongside the built-in ones | Also joins the list — the built-in implementation keeps running too |
| Keyed | `IJobHandler` | Order does not matter; the handler is found by its key, not by position | Same — but a plain `AddSingleton<IJobHandler, T>()` registers nothing the dispatcher can find |

The dedicated `Add*()` methods (`AddAgentDecorator`, `AddAgentSource`,
`AddRunJudge`, `AddContentGuard`, `AddModelProvider`) exist for the
multi-registration seams precisely because getting this wrong there is
silent: a decorator you meant to *replace* the built-in one instead runs
*alongside* it, and both apply.

`IJobHandler` is **keyed**, not multi-registration, and its `Add*()` method is
not optional. A job names the handler it needs, so the handler is registered
against that name:

```csharp
builder.Services.AddJobHandler<NightlyReportJobHandler>("contoso.nightly-report");
```

Writing `services.AddSingleton<IJobHandler, NightlyReportJobHandler>()` instead
compiles and appears to work: nothing is registered under a key, so the
dispatcher finds no handler and the first job queued for that name fails. A single-instance seam has no such trap — a
consumer's own `services.AddSingleton<IProviderRetryClassifier, T>()` simply
overrides the default, in either order — which is why those extension points
have no dedicated registration method: the plain escape hatch already behaves
the way you would expect.

```csharp
// Multi-registration seam: joins the list, does not replace anything.
builder.AddTracon()
       .AddAgentDecorator<AuditingAgentDecorator>();

// Single-instance seam: this line alone replaces the built-in classifier,
// whether it runs before or after AddTracon().
builder.Services.AddSingleton<IProviderRetryClassifier, AcmeRetryClassifier>();
```

## Read next

- [Runs and recording](/concepts/runs/) — the decorator that gives every run its record
- [Write your own agent source](/guides/write-your-own-agent-source/) — the sibling multi-registration seam for the catalog's data, not its decoration
- [Reference: IAgentDecorator](/api/tracon.iagentdecorator/) — full API contract
