Skip to content
Tracon

AgentRunBudget

Namespace Tracon · Assembly Tracon.Abstractions.dll

A run budget shared across an entire call tree.

public sealed class AgentRunBudget

objectAgentRunBudget

object.GetType(), object.ToString(), object.Equals(object?), object.Equals(object?, object?), object.ReferenceEquals(object?, object?), object.GetHashCode()

This type is deliberately a class, not a record. The budget is shared mutable state: every run in the tree uses the same instance. A record invites copying; a copied budget would give each branch its own limit, and the limit would lose its meaning.

Counters increment lock-free (Threading.Interlocked). Child runs start concurrently: Microsoft Agent Framework’s background agents run without blocking, so the same budget is read and written from multiple threads.

AgentRunBudget.TryReserveRun (the count and depth dimensions) only blocks a new child run from starting; it never interrupts a child run already in progress — cutting one off midway would leave the model with an incomplete context and would also corrupt the root run.

The token, cost, and duration dimensions are enforced the same way, by a decorator installed inside the tool-call loop (RunBudgetChatClient, internal to Tracon.Core): once AgentRunBudget.IsExhausted is true, that decorator refuses the tree’s next model call, so a long-running tool loop is cut off mid-run rather than only at its next child call. The cutoff always lands between two model turns, never inside one — the decorator only ever refuses a call it has not yet made. A tool that itself runs long is not interrupted; the cutoff waits for that tool call to finish and only then refuses the model call that would follow it.

Creates a new run budget.

public AgentRunBudget(TimeSpan? maxDuration = null, TimeProvider? timeProvider = null)

maxDuration TimeSpan?

The wall-clock time the whole tree may take, taken at face value: unlike the four init dimensions below, null is the only value that means “no limit” — TimeSpan.Zero or a negative value produces a AgentRunBudget.Deadline that has already passed, the same way a raw MaxTotalTokens = 0 reads as “exhausted from the start” rather than “unlimited”. The “zero/negative means unlimited” convention is applied one layer up, in TraconAgentGraphOptions.CreateBudget (Tracon.Core).

timeProvider TimeProvider?

The time source AgentRunBudget.Deadline is computed from and every later expiry check reads. Defaults to TimeProvider.System.

The amount spent across the tree so far.

public decimal ConsumedCost { get; }

decimal

The tokens spent across the tree so far.

public long ConsumedTokens { get; }

long

The instant the tree’s time budget runs out. Computed once, when this budget was constructed; every run in the tree shares the same value. null when AgentRunBudget.MaxDuration is null.

public DateTimeOffset? Deadline { get; }

DateTimeOffset?

Whether the cost limit has been exceeded.

public bool IsCostBudgetExhausted { get; }

bool

Whether the tree’s AgentRunBudget.Deadline has passed.

public bool IsDurationBudgetExhausted { get; }

bool

Whether the tree has spent past its token, cost, or time limit. Does not reflect AgentRunBudget.MaxTotalRuns: the run-count limit only blocks starting a new child run (AgentRunBudget.TryReserveRun), it says nothing about whether the current run may keep calling its model.

public bool IsExhausted { get; }

bool

Whether the token limit has been exceeded.

public bool IsTokenBudgetExhausted { get; }

bool

The maximum allowed call depth. The root run is 0, so the default value allows a three-layer tree.

public int MaxDepth { get; init; }

int

The wall-clock time the whole tree may take, counted from when this budget was constructed. If null, there is no time limit.

public TimeSpan? MaxDuration { get; }

TimeSpan?

The maximum amount spendable across the tree. If null, there is no cost limit.

public decimal? MaxTotalCost { get; init; }

decimal?

Cannot be enforced when a model call’s pricing is undefined (PricingSource.Unknown); the token limit applies instead, the same fallback QuotaDefinition.MaxCost uses.

The maximum number of child runs that may start. The root run is not counted toward this number. If null, there is no count limit.

public int? MaxTotalRuns { get; init; }

int?

The maximum tokens spendable across the tree. If null, there is no token limit.

public long? MaxTotalTokens { get; init; }

long?

The number of child runs started so far.

public int StartedRuns { get; }

int

Produces a user-facing text describing the limit that was exceeded.

public string DescribeExhaustion()

string

Text stating which limit was exceeded.

The text is returned to the model as a tool result. Saying “budget exhausted” is not enough; unless the exceeded limit is named, the user cannot see which setting to raise.

Produces a user-facing text describing the token, cost, or time limit that cut a model call short mid-run.

public string DescribeModelCallExhaustion()

string

Only called once AgentRunBudget.IsExhausted is true; the run-count limit does not apply here (see AgentRunBudget.IsExhausted).

Records the number of tokens spent into the budget.

public void RecordUsage(long tokens)

tokens long

The number of tokens to add. A negative value is ignored.

Records the tokens and cost spent in one model turn into the budget.

public void RecordUsage(long tokens, decimal? cost)

tokens long

The number of tokens to add. A negative value is ignored.

cost decimal?

The cost to add, or null when the turn’s price is undefined (PricingSource.Unknown) — nothing is added to AgentRunBudget.ConsumedCost in that case, so an installation with an unpriced model never trips a cost limit it cannot actually measure.

Reserves budget room for a new child run.

public bool TryReserveRun()

bool

true if room was reserved; false if the token, cost, time, or count limit has been exceeded.

The counter increments only when room is reserved. If a failed attempt incremented the counter, every new attempt in a tree that has hit its limit would keep growing the count, and the UI would show runs that never actually started.