Skip to content
Tracon

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.

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);
}

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.

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.

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.

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.