ExAgent.UsageLimits (ExAgent v2.2.0)

View Source

Execution-tree safety net: each run's limits also constrain its framework descendants. Admission is serialized by ExAgent.ExecutionScope; these functions provide the pure checks. Token/cost thresholds are retrospective, so already in-flight requests can exceed them.

Any nil threshold is unchecked. When a limit is exceeded the run terminates with a RunError with reason {:usage_limit_exceeded, which, value}.

  • request_limit / total_tokens_limit / input_tokens_limit / output_tokens_limit — checked before each model request.
  • tool_calls_limit — checked before a batch of tool calls is executed (pydanticAI semantics: if the model returned parallel calls that would exceed the limit, none run).
  • max_budget_cents — checked before each model request against an estimated cost (see ExAgent.CostGuard). An explicit :estimate_cost callback takes precedence over a qualified public upstream estimate; unknown/error never silently falls back. Neither is provider billing.

accounting: :strict (default) requires sufficient Model-reported metrics for enabled thresholds. A declared normalized-only contract rejects before IO; unknown contracts are checked against actual reports before later effects. accounting: :estimated opts into available subtotals and may continue without metrics/pricing, but metric limits require a finite request_limit in this scope or an ancestor. Host-only limits require no accounting. A child's policy cannot weaken its ancestors. In-flight operations can exceed retrospective thresholds.

Example

Here model is an explicitly configured ExAgent.Models.ReqLLM instance; normalized metrics require opt-in estimated thresholds and a finite request bound.

alias ExAgent.{CostGuard, UsageLimits}

agent =
  ExAgent.new(
    model: model,
    usage_limits: %UsageLimits{
      accounting: :estimated,
      request_limit: 5,
      total_tokens_limit: 2000,
      tool_calls_limit: 10,
      max_budget_cents: 25
    }
  )

pricing = CostGuard.estimator(%{input_per_1k_cents: 250, output_per_1k_cents: 1000})
ExAgent.run(agent, "research this", estimate_cost: pricing)

Summary

Functions

Check the request/token/budget limits against accumulated usage, the upcoming request count, and the estimated cost_cents. Returns :ok or {:error, {:usage_limit_exceeded, which, value}}.

Check the tool_calls_limit before executing a batch of incoming tool calls. Returns :ok or {:error, {:usage_limit_exceeded, :tool_calls, n}}.

Validate nonnegative integral counters and a nonnegative monetary threshold.

Types

t()

@type t() :: %ExAgent.UsageLimits{
  accounting: :strict | :estimated,
  input_tokens_limit: non_neg_integer() | nil,
  max_budget_cents: number() | nil,
  output_tokens_limit: non_neg_integer() | nil,
  request_limit: non_neg_integer() | nil,
  tool_calls_limit: non_neg_integer() | nil,
  total_tokens_limit: non_neg_integer() | nil
}

Functions

check_before_request(limits, usage, request_count, cost_cents \\ nil)

@spec check_before_request(
  t(),
  ExAgent.Message.Usage.t(),
  non_neg_integer(),
  number() | nil
) ::
  :ok | {:error, term()}

Check the request/token/budget limits against accumulated usage, the upcoming request count, and the estimated cost_cents. Returns :ok or {:error, {:usage_limit_exceeded, which, value}}.

request_count is the number of Model attempts already admitted (0 before the first). cost_cents is the known estimate so far, or nil when unknown; an strict monetary budget rejects unknown cost; estimated thresholds use available subtotals, with host-bound enforcement in ExecutionScope.

check_tool_calls(usage_limits, executed, incoming)

@spec check_tool_calls(t(), non_neg_integer(), non_neg_integer()) ::
  :ok | {:error, {:usage_limit_exceeded, :tool_calls, non_neg_integer()}}

Check the tool_calls_limit before executing a batch of incoming tool calls. Returns :ok or {:error, {:usage_limit_exceeded, :tool_calls, n}}.

executed is the historical parameter name for admitted/reserved tool calls, not dispatched calls, successes or external effects. Following pydanticAI, if executed + incoming would exceed the limit, none of the incoming calls are executed.

validate(limits)

Validate nonnegative integral counters and a nonnegative monetary threshold.