DEV Community

DarianReed1254
DarianReed1254

Posted on

Estimate Cost Before an Expensive Agent Step — Compare the Remaining Budget

A page that says daily budget exhausted arrives too late. The invoice exposure already exists, the developer-tools workload is stalled, and the on-call engineer cannot tell which credential admitted the work. The least complex useful control is a synchronous admission gate: estimate the next costly step, compare it with spend already settled plus spend currently reserved, and reject the step before execution if the credential's limit would be crossed.

Short answer: treat estimated cost as a reservation, not a dashboard forecast. Keep one atomic ledger per credential, reserve before dispatch, settle actual usage afterward, and release the reservation on cancellation or a failed dispatch. The decision is estimated <= limit - settled - reserved; a plain estimated <= limit - settled check permits concurrent workers to approve the same remaining money.

That distinction determines blast radius. A shared credential can expose the whole account limit, while a workload-scoped credential with its own ledger can expose only its assigned cap. Dashboards help investigate later. They do not make admission atomic.

What page should fire before the invoice does?

Consider the alert-to-action trace for a code-review agent in a developer-tools platform. The bad page is a lagging one: credential spend limit exhausted, with a credential identifier, a limit, and no useful indication of work still in flight. By then, retrying workers may be holding reservations or may already have dispatched metered calls. The responder has to reconstruct concurrency while deciding whether disabling the credential will interrupt unrelated repositories.

The earlier signal is reservation pressure. Page when a specific credential has little unreserved capacity and active work is still attempting expensive steps; attach the credential ID, settled amount, reserved amount, configured limit, oldest reservation age, and rejected-admission count. Do not page merely because cumulative spend is high. High utilization with no rejected work is capacity planning, while repeated admission rejection for an active workload is an operational event.

Ask one question first: what page fired? If the answer is an account-wide invoice threshold, the control boundary is already too broad for diagnosing one workload. The alert key should match the unit that can be disabled or reconfigured without stopping its neighbors. In this design, that unit is the credential.

A useful event is small enough to inspect and stable enough to aggregate:

package budget

import "time"

type AdmissionEvent struct {
    CredentialID   string
    WorkloadID     string
    EstimateMicros int64
    SettledMicros  int64
    ReservedMicros int64
    LimitMicros    int64
    Decision       string
    ObservedAt     time.Time
}
Enter fullscreen mode Exit fullscreen mode

Money is represented as integer micros here, not floating point. The particular scale is a local contract; consistency matters more than the name. The event deliberately omits the secret itself. OWASP recommends limiting access to secrets, rotating them, and applying least privilege, which supports using a non-secret credential identifier as the accounting and alerting key rather than copying credential material into logs.

How should an agent estimate cost and compare the remaining budget before an expensive step?

A preflight check followed by a separate write has a race. Suppose a credential has 100 units left and two workers each estimate 70. Both read 100, both approve, and the admitted exposure becomes 140. No arithmetic refinement repairs that sequence; reservation must be an atomic compare-and-update operation in the authoritative ledger.

The race is the incident.

The loop below keeps the contract visible. TryReserve must serialize decisions for one credential, commonly with a database transaction, a conditional update, or a single-owner ledger process. It returns an opaque reservation ID so settlement and release are idempotent operations rather than guesses based on workload names.

package budget

import (
    "context"
    "errors"
)

var ErrBudgetDenied = errors.New("credential budget denied")

type Estimate struct { Micros int64 }
type Usage struct { ActualMicros int64 }

type Ledger interface {
    TryReserve(context.Context, string, Estimate) (string, bool, error)
    Settle(context.Context, string, Usage) error
    Release(context.Context, string) error
}

type ExpensiveStep interface {
    Estimate(context.Context, []byte) (Estimate, error)
    Run(context.Context, []byte) (Usage, error)
}

func RunOne(ctx context.Context, ledger Ledger, credentialID string, step ExpensiveStep, input []byte) error {
    estimate, err := step.Estimate(ctx, input)
    if err != nil { return err }
    if estimate.Micros < 0 { return errors.New("negative estimate") }

    reservationID, admitted, err := ledger.TryReserve(ctx, credentialID, estimate)
    if err != nil { return err }
    if !admitted { return ErrBudgetDenied }

    usage, err := step.Run(ctx, input)
    if err != nil {
        if releaseErr := ledger.Release(ctx, reservationID); releaseErr != nil {
            return errors.Join(err, releaseErr)
        }
        return err
    }
    return ledger.Settle(ctx, reservationID, usage)
}
Enter fullscreen mode Exit fullscreen mode

This is intentionally stricter than an estimate printed before a call. Fail closed when the ledger is unavailable: allowing work through because accounting timed out converts a telemetry outage into unbounded financial exposure. That trade-off may reduce availability for the affected credential, but it preserves the cap that the gate claims to enforce.

There is an uncomfortable edge: actual usage can exceed the estimate. Settlement must record the full actual amount even when it pushes the ledger over its limit; hiding the overage corrupts the next decision. The next admission is then denied. To keep the bound meaningful, the estimator needs a defensible upper bound derived from the step's configured maximum work, not an optimistic average. If no upper bound can be established, place a conservative ceiling on the step or treat it as inadmissible.

Reject it.

Credential scope is the real control plane

An account-wide counter answers an accounting question. It does not answer the incident question: which single workload can still increase exposure? Mapping one credential to one independently capped workload makes revocation, alert ownership, and spend admission describe the same boundary.

Boundary Failure blast radius On-call action Hidden coupling
One account credential Every workload sharing it Disable or raise the shared limit A noisy job consumes capacity needed by unrelated jobs
Credential per environment All workloads in that environment Isolate an environment Repositories still compete inside the environment
Credential per workload One agent or job class Stop or recap that workload More credentials and ledgers must be rotated and audited

The narrowest boundary is not free. Credential inventory grows, rotation needs automation, and abandoned workloads can leave stale credentials. Still, those are tractable lifecycle costs. A broad credential saves setup work by transferring ambiguity to the worst possible moment: an alert where the responder cannot isolate the offender without collateral damage.

Least privilege applies to budget authority as well as API permissions. A credential allowed to invoke an expensive operation should not inherit the entire organization's financial headroom by accident. Give it the smallest limit consistent with its workload, and make increasing that limit an explicit control-plane change rather than a retry behavior inside the agent.

Instrument the gate, not just the bill

The reservation ledger is the source of admission truth. Metrics and logs are projections of that truth, useful only if their dimensions let a responder reconcile an alert without exposing secrets. At minimum, record decisions by credential ID and workload ID, reservation age, estimated versus actual cost, settlement failures, releases, and denied attempts. Keep cardinality bounded: request IDs belong in traces or structured logs, not metric labels.

Test the state machine under concurrency, not only its arithmetic. A meaningful test launches competing reservations whose sum exceeds the remaining capacity and proves that the accepted total never crosses the limit. Then cover duplicate settlement, duplicate release, cancellation between reservation and dispatch, process death after dispatch, and reservation expiry. Expiry requires care: releasing an old reservation while its remote step is still running can admit replacement work and break the cap. A lease should be renewed by an owner that knows whether execution remains live; elapsed time alone is weak evidence.

Deployment deserves the same suspicion. Introduce the ledger in observe-only mode to compare estimates with actual usage, but do not describe observation as enforcement. Once estimate error is understood, enable denial for one credential class, verify reconciliation, and expand the boundary gradually. Keep a separate, audited administrative path for changing limits. An agent must never raise its own cap in response to ErrBudgetDenied.

The operational invariants are compact:

  • settled + reserved is the amount unavailable for new admission.
  • Accepted reservations for a credential are serialized.
  • Every reservation reaches exactly one terminal accounting outcome, even when messages are retried.
  • Actual usage is recorded without clipping.
  • Logs contain credential identifiers, never credential secrets.

Where should the threshold sit?

A threshold at 100% maximizes usable capacity but leaves no room for estimation error or already-running work that reports usage late. A lower threshold creates headroom, yet setting it too low turns harmless variance into denied developer workflows and noisy pages. There is no universal percentage supported by the problem statement, so choose the margin from observed estimate error and reporting delay for each step class.

This is where distrust of a polished dashboard pays off. A percentile chart can hide the rare underestimate that defines the cap violation. Inspect the error distribution's upper tail, correlate it with input size and configured maximum output, and alert on sustained admission pressure rather than one rejection. The page should carry enough ledger state to decide whether to stop a workload, adjust its explicit limit, or repair stuck reservations.

False positives have a real cost: responders learn that budget pages mean routine throttling, then the alert that signals a stuck reservation or a runaway retry receives the same tired glance. Separate a denied request, which software should handle as a normal control decision, from a page, which should mean the credential cannot serve its intended workload or the accounting state is unhealthy.

The final rule is blunt. Cap the credential, reserve atomically, and page on actionable pressure. Anything less is cost reporting after the blast radius has already been chosen.

Further reading

Top comments (0)