Extend
Write your own agent source
IAgentSource is the catalog extension point. Use it when agent definitions do not
live in the management database, or when an agent is backed by another runtime.
Register it through AddAgentSource(); the catalog keeps the normal decorator chain,
so recording, telemetry, and approval behavior still apply.
A complete, buildable definition source lives in
samples/Tracon.Samples.CustomAgentSource — it reads AgentDefinition JSON files
from a directory, uses only published packages (PackageReference, not a
project-internal type), and its test project runs the official contract suite plus a
real agent run. Read its source alongside this guide.
AddAgentSource has three overloads. The generic one resolves the source’s own
constructor dependencies (including its own settings, registered separately) through DI:
builder.Services.AddSingleton(new GitAgentSourceOptions { RepositoryUrl = url });builder.AddTracon() .AddAgentSource<GitAgentSource>();Pass a ready instance, or a factory, when the source needs something that does not belong in the container as its own singleton:
builder.AddTracon().AddAgentSource(sp => new GitAgentSource(repositoryPath, sp.GetRequiredService<AgentDefinitionCompiler>()));All three register the source as a singleton; calling the generic overload twice for the same type registers it once.
Choose the source shape
Section titled “Choose the source shape”| Shape | Use it when | Resolve behavior |
|---|---|---|
| Definition source | You own an AgentDefinition in Git, object storage, or another database |
Call AgentDefinitionCompiler.CompileCachedAsync() |
| Agent source | You already own a runnable MAF AIAgent |
Return the agent directly |
For a definition source, let the compiler own dependency fingerprints and the BYOK cache bypass. Do not repeat that logic in your source.
flowchart LR
accTitle: The two source shapes
accDescr: A definition source hands an AgentDefinition to the compiler, which builds the agent with the model rings, the tenant key and the cost. An agent source builds its own agent; it gets the same model rings, tenant key and egress policy from CreateChatClientAsync, and the cost from the model named in its descriptor. The catalog applies Tracon's decorators to both.
DEF["Definition source<br/>AgentDefinition"] --> COMP["AgentDefinitionCompiler<br/>CompileCachedAsync"]
COMP --> CAT["Catalog decorators"]
OWN["Agent source<br/>your AIAgent"] --> CLIENT["CreateChatClientAsync<br/>rings · tenant key · egress"]
OWN --> DESC["AgentDescriptor.Model<br/>cost"]
CLIENT --> CAT
DESC --> CAT
For an agent source, the compiler does not run, so two things are yours. Build the
chat client with IModelProviderRegistry.CreateChatClientAsync inside
ResolveAsync: it applies the tenant’s own provider key and egress policy, and it
adds the same model rings a compiled agent gets. Set AgentDescriptor.Model in
ListAsync: the run record reads the model from the descriptor, and without it the
run has no cost. Do not cache an agent whose client carries a tenant’s own key
across tenants; HasTenantProviderOverrideAsync tells you when it does.
samples/Tracon.Samples.MafFeatures builds such a source, and its tests show the
tenant’s key reach the chat client and the cost reach the run record. Use any
Microsoft Agent Framework feature says when to choose this
shape over a factory agent.
public async ValueTask<AIAgent?> ResolveAsync(string name, string? culture = null, CancellationToken ct = default){ var definition = await _repository.GetAsync(name, ct); return definition is null ? null : await _compiler.CompileCachedAsync(definition, _cache, _tenants.TenantId, culture, ct);}Source contract
Section titled “Source contract”Sources are singleton and calls can run concurrently. Keep no request or run state in
fields, and do not capture scoped services. ListAsync() is on the run path: it must
be repeatable and side-effect free. Tracon does not add a global snapshot, timeout,
retry, or circuit breaker for a source.
ListAsync() and ResolveAsync() must describe the same agent set: a name
ResolveAsync() resolves has to appear in ListAsync(), and vice versa. Return stable
descriptors and never mutate one — or its nested collections — after returning it. The
catalog defensively copies each descriptor at its own boundary too, but that is
belt-and-suspenders, not a license to violate the contract; a source that shares a
mutable list with the catalog is still a bug the copy merely contains.
If the source builds agents from an AgentDefinition, copy the fields the host reads off the
descriptor: SkillNames and Skills for the A2A and MCP exposure check, and ExtensionNames
for the catalog. A descriptor that names skills and leaves Skills empty reads as “the read
approval is on”, and the host refuses to expose the agent even when the definition turned the
approval off. A descriptor without ExtensionNames lists an agent compiled with an extension
as one without it.
Honor the CancellationToken you are given — Tracon adds no timeout of its own, so
an uncancellable source blocks its caller indefinitely. A genuine
OperationCanceledException must propagate; do not catch and convert it into anything
else. “Genuine” means the token you were given is cancelled: an
OperationCanceledException while it is not — your own HTTP client timing out surfaces
as TaskCanceledException — is a source failure like any other, so listing skips the
source and resolution fails with TraconAgentSourceException.
A source can be global or tenant-aware; when it reads ITenantContext, any local cache
must include the tenant identity. Startup and background calls see the default tenant.
Priority and management API
Section titled “Priority and management API”Smaller priority values win a name collision. AgentSourcePriority.Code (0) and
AgentSourcePriority.Database (100) are the values Tracon’s own built-in sources
use, not values a custom source is barred from choosing. Pick 1 through 99 to run
between code and database definitions, or 101 or greater to run after the database.
Choosing 0 or 100 ties with the matching built-in source instead, and DI
registration order breaks the tie — the same rule as any other equal-priority pair.
Set custom descriptors to AgentDefinitionOrigin.Custom. They appear in the console
but are read-only: the management API cannot update or delete a definition that your
source owns.
Prove the implementation
Section titled “Prove the implementation”using Tracon.Testing.Contracts.AgentSources;
public sealed class GitAgentSourceTests : AgentSourceContract{ protected override string KnownAgentName => "support"; protected override ValueTask<IAgentSource> CreateSourceAsync() => new(new GitAgentSource(/* test repository and services */));}Derive VersionedAgentSourceContract when your source implements
IVersionedAgentSource. Derive TenantAwareAgentSourceContract when its list changes
by tenant. The suite checks repeatability, concurrency, cancellation, descriptor
integrity, and list/resolve consistency without a network call.
Scale and operations
Section titled “Scale and operations”GET /api/agents is not paged. Resolution also scans the winning source’s list, and
version resolution can list each source until it finds the owner. A source holding many
definitions should keep its own correctly invalidated cache. Source failures appear in
logs, diagnostics, and the tracon.agent_source.failures metric. List failures are
isolated; resolution failures stop so a lower-priority agent cannot run by mistake.
Read next
Section titled “Read next”- Agents — the catalog and agent-definition model
- Model providers — write a custom model provider
- Reference: IAgentSource — full API contract