Skip to content
Tracon

The .NET control plane for Microsoft Agent Framework.

A package family that runs inside your own ASP.NET Core host. MAF runs your agents. Tracon records them, scopes them to a tenant, and governs what they may call.

dotnet add package Tracon --prerelease

Production-oriented preview packages are on NuGet and npm. No stable version yet. Source on GitHub

RUN 01a0d2af…a47esupport · acmeSample

  1. 0run.startedT+00:00.048"Please cancel ORD-7. I ordered it twice."
  2. 1tool.invokingT+00:01.400get_order_status orderId=ORD-7
  3. 2tool.invokedT+00:01.401"Order ORD-7 has shipped."
  4. 3message.completedT+00:01.402"" · a tool call, no text
  5. 4run.awaiting-inputT+00:01.406[{"requestId":"…","toolName":"cancel_order"}]Holding

AwaitingApproval · cancel_order has not run

Sample run. Real event names.

Two lines to the tower.

Stores start in memory and the host starts without a model key. The console and API live under /tracon, and until you allow remote access, only local callers reach the data endpoints.

Program.cs
using Tracon;

var builder =
    WebApplication.CreateBuilder(args);
builder.AddTracon().UseUI();

var app = builder.Build();
app.MapTracon(); // at /tracon
app.Run();

20 modular packages176 generated HTTP operations30 embedded console screens

MAF flies the aircraft. Tracon runs the airspace.

AIAgent, AgentSession, ChatMessage and AIFunction pass through unwrapped. There is no parallel type hierarchy to adopt.

One request, four questions.

A customer asks the support agent to cancel order ORD-7. The order has shipped, and the model proposes cancel_order anyway. Follow that one sample request to its filed record.

01 / Separate T+00:00.003

Who owns this request?

acme · matched by the API key

The request carries an API key bound to tenant acme, and the key outranks any claim or header. From here on, tenant-scoped queries keep each tenant’s records logically isolated. Your host signs operators in; Tracon reads their tenant_id claim, and once a claim type is set, it never reads the header.

Program.cs
builder.AddTracon()
    .UseTenancy(o =>
    {
        o.Enabled = true;
        o.ClaimType = "tenant_id";
    });

Set the boundary

02 / Clear T+00:01.406

Who may call this tool?

No one yet. AwaitingApproval

The policy is set in code, not by the model. The tool requires approval, so the queued run closes AwaitingApproval and the call waits with its arguments, for 24 hours by default. A tool is a method: the console can select it but never write it.

Define the tool policy

No clearance, no takeoff.

Tools/OrderTools.cs
[TraconTool("cancel_order",
    "Cancels an order.",
    RequiresApproval = true,
    Effect = ToolEffect.Destructive)]
public static string CancelOrder(
    [Description("The order id.")]
    string orderId)
    => $"Order {orderId} is canceled.";
The console's Approvals screen holding cancel_order for order ORD-7: Cancel order ORD-7 for Jordan Rivera, with Approve and Reject.The console's Approvals screen holding cancel_order for order ORD-7: Cancel order ORD-7 for Jordan Rivera, with Approve and Reject.
The console’s Approvals screen with the same call. Captured from a local sample with seeded data. This is sample data, not live telemetry.

03 / Recover T+04:12.300

What survives a restart?

The hold. The decision later opens R2.

Program.cs
builder.AddTracon()
    .UsePostgreSql(connectionString);

At T+01:30 the host restarts; the held run and the pending call are rows in PostgreSQL. After the restart, ops@acme.test rejects the call. The audit entry is written first, and the decision applies only if that write succeeds.

The decision opens R2 in session s-7f3a, where the model reads the refusal. R1 keeps its status: run history is append-only. A client that lost the 202 retries with the same Idempotency-Key and gets the stored response.

Plan the recovery

04 / Record T+04:14.118

What happened, and who changed what?

Valid · audit trail, 3 entries checked

Each run is a numbered event tape: sequence numbers start at 0, leave no gaps, and resume after Last-Event-ID when a stream drops. These are persisted run records. Recording is best-effort, so a store failure costs the record, not the run.

Read a recorded run

The console's run detail for a seeded run: the reserve_stock call marked failed, with its argument orderId=ORD-9.The console's run detail for a seeded run: the reserve_stock call marked failed, with its argument orderId=ORD-9.
A seeded run in the console: reserve_stock failed, with its arguments.

Audit trail · tenant acmeSample

Runs are not audit entries; the run history is the record. The audit trail records who changed what: the tenant, the key, the decision. Each entry stores its SHA-256 and the previous entry’s hash.

GET /tracon/api/audit/verify
{"status":"Valid",
 "entriesChecked":3,
 "firstFailingEntryId":null}{"status":"Broken",
 "entriesChecked":3,
 "firstFailingEntryId":
  "01a0d2b3-85e6-719a-ae24-c8b3d1e0f571"}

Verification is an API call; the console does not run it. Tamper detection

Thirty minutes, one machine: 12 515 runs accepted, 12 515 completed, no gap in any event stream, nothing under the wrong tenant. Not an SLA.

  1. prev null

    #1 tenant.create tenant:acme

    hash ef98b97e…f3b3

  2. prev ef98b97e…f3b3

    #2 apikey.create apikey:01a0d2a2…2b10

    hash 5f2e91af…08ff

  3. prev 5f2e91af…08ff

    #3 approval.decision tool:cancel_order

    after {"approved":false,…}{"approved":true,…}

    hash de2fed3b…fbd5 ≠ recomputed b4c99378…f65c

Autonomy, under clearance.

The model proposed. Your code set the policy. A person decided, and the record kept each step.

Most governance is off until you turn it on. Before production, make the host refuse to start while a decision is still open.

Program.cs
builder.AddTracon()
    .RequireProductionProfile();
Host output at startup
RequireProductionProfile() was called, and 6 production decisions are still on the permissive default, so the host does not start.

The production decisions

Your host owns the boundary: authentication, model providers, storage and outbound connections. The public API is not frozen, and Tracon.AspNetCore uses preview MAF hosting packages. Architecture and limits · Compatibility · Versioning · Licensing

Coming from DevUI? Microsoft advises you to “build your own custom interface” for production. Tracon is one implementation of that production-facing layer. Compare DevUI and Tracon