budget

package
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 7, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package budget is the agent's spend ceiling: a per-run pool of tokens and cost that every model and tool call is charged against at the dispatch waist, so a run, or a whole fan-out of runs sharing one pool, cannot exceed the limit set for it. It is the enforcement half of the README's "shared budget pool"; the metering it charges is the dispatch.Metering each governed action already reports.

A budget is an ordinary Resource (kind "Budget") keyed by the run id, so it is durable, replayable, and shared: concurrent runs charge the same record under optimistic concurrency, and a crash-resumed run reads the spend already made rather than a fresh pool. A run with no budget bound is unlimited, which keeps the standalone agent zero-config; binding one switches the posture to default-deny once the ceiling is reached.

Index

Constants

View Source
const (
	// GroupVersion is the Budget kind's API group and version.
	GroupVersion = "budget.ionagent.io/v1alpha1"
	// Kind is the resource kind name.
	Kind = "Budget"
)

Variables

This section is empty.

Functions

func FromContext

func FromContext(ctx context.Context) (string, bool)

FromContext returns the budget id bound to ctx and whether one was present. Absent an id the run is unbudgeted (unlimited), the zero-config default.

func Into

func Into(ctx context.Context, id string) context.Context

Into returns a context carrying the budget id (the run id) a run charges against, so the dispatch waist's Hook reads the pool from the context rather than from a parameter. Binding it once at the top of a run applies the budget to every action that run dispatches.

func RegisterKind

func RegisterKind(reg *resource.Registry) error

RegisterKind registers the Budget kind so budgets can be stored and admitted like any other resource.

Types

type Hook

type Hook struct {
	// contains filtered or unexported fields
}

Hook enforces a run's budget at the dispatch waist. Before rejects an action when the run's pool is already spent to its ceiling; After charges the action's metering onto the pool. Because the waist governs every model and tool call, one Hook caps the whole run with no per-call wiring. It composes with the capability admitter rather than replacing it: capability decides what may run, budget decides whether there is still budget to run it.

With a reservation configured (WithReservation), Before instead reserves an upper-bound estimate against the pool before admitting, and After settles the reservation into the actual metered spend. This closes the concurrent-overshoot gap a plain check-then-charge leaves: when many actions share one pool (a fan-out of children), each would otherwise read the same under-budget snapshot and all pass; the atomic reserve makes them admit against one consistent view.

func NewHook

func NewHook(store resource.Store, opts ...HookOption) *Hook

NewHook returns a budget Hook backed by store. Add it to a dispatcher with dispatch.WithHook to budget every action that dispatcher governs.

func (*Hook) After

func (h *Hook) After(ctx context.Context, a dispatch.Action, m dispatch.Metering, _ error)

After records the action's spend. In reservation mode it settles the reservation into the actual metering (releasing the estimate and charging the actual in one write, so a zero metering simply releases the reservation); otherwise it charges the metering. It is best effort: the work has already run, so a write that fails to persist is surfaced through the dispatcher's observability rather than failing the action.

func (*Hook) Before

func (h *Hook) Before(ctx context.Context, a dispatch.Action) error

Before rejects an action whose run has exhausted its budget. In reservation mode it atomically reserves the estimate and rejects when the pool is fully committed; otherwise it reads availability. With no budget bound it admits everything, so an unbudgeted run is unconstrained.

type HookOption

type HookOption func(*Hook)

HookOption configures a budget Hook.

func WithReservation

func WithReservation(est Spent) HookOption

WithReservation makes the Hook reserve an upper-bound estimate against the pool before each action and settle the actual after, instead of a plain check-then- charge. The estimate should be an upper bound on a single action's spend (for a model call, the input plus the max output tokens), so the pool can never be exceeded even under a concurrent fan-out. A zero estimate (the default) keeps the original check-then-charge behaviour.

type Ledger

type Ledger struct {
	// contains filtered or unexported fields
}

Ledger reads and writes run budgets on the resource store. It is the durable home of a run's spend: concurrent charges converge on one record under the store's optimistic concurrency, so a shared pool stays correct across a fan-out and across a crash.

func NewLedger

func NewLedger(store resource.Store) *Ledger

NewLedger returns a Ledger backed by store.

func (*Ledger) Available

func (l *Ledger) Available(ctx context.Context, id string, scope resource.Scope) (bool, error)

Available reports whether the run identified by id still has budget: true when no budget is bound (unlimited), and true until the recorded spend reaches a set limit. It is the pre-execution check the dispatch waist gates an action on.

func (*Ledger) Charge

func (l *Ledger) Charge(ctx context.Context, id string, scope resource.Scope, m dispatch.Metering) error

Charge adds m to the run's recorded spend, retrying under optimistic concurrency so concurrent charges against a shared pool all land. A run with no budget bound is a no-op (unlimited). Charging more than the limit is allowed: the limit is enforced before an action runs (see Available), and the actual cost is only known after, so the recorded spend is the truth and can settle slightly past the ceiling.

func (*Ledger) Open

func (l *Ledger) Open(ctx context.Context, id string, scope resource.Scope, limits Limits, owners ...resource.OwnerReference) (resource.Resource, error)

Open creates (or returns the existing) budget for run id in scope, capped by limits. Pass owners to bind the budget to the run that owns it (an OwnerReference to the run's goal), so the budget is garbage-collected when the run ends rather than outliving it.

func (*Ledger) Release

func (l *Ledger) Release(ctx context.Context, id string, scope resource.Scope, est Spent) error

Release returns a reservation to the pool without spending it, for an action that was admitted but did not run (rejected downstream, cancelled). It floors the reserved total at zero so a doubled release under a race cannot drive it negative.

func (*Ledger) Reserve

func (l *Ledger) Reserve(ctx context.Context, id string, scope resource.Scope, est Spent) (bool, error)

Reserve atomically holds est against the run's pool before an action runs: the reserve half of reserve-before-dispatch. It admits (returns true) while the pool still has budget left, where "left" means the committed total (spent plus already-reserved) has not reached a set limit, and records the reservation; it refuses (false) once the pool is fully committed. Because the check and the reservation are one compare-and-set, concurrent actions sharing a pool admit against one consistent view rather than each reading an under-budget snapshot and overshooting together. A run with no budget bound is unlimited: always admits, records nothing. With an upper-bound estimate the ceiling cannot be exceeded; with a smaller estimate the overshoot is bounded by the in-flight estimates.

func (*Ledger) Settle

func (l *Ledger) Settle(ctx context.Context, id string, scope resource.Scope, est Spent, m dispatch.Metering) error

Settle converts a reservation into actual spend once an action finishes: it releases est and charges the metered actual in one atomic write, so the pool never briefly double-counts (reserved and spent) nor under-counts (released before charged). A zero est with a real metering behaves like Charge.

type Limits

type Limits struct {
	Tokens int64   `json:"tokens,omitempty"`
	Cost   float64 `json:"cost,omitempty"`
}

Limits is a run's spend ceiling. A zero field means no limit on that axis, so the zero Limits is unlimited (a budget that only tracks spend without capping it). Tokens is a total token count; Cost is in the provider's currency unit.

func (Limits) Exceeded

func (l Limits) Exceeded(s Spent) bool

Exceeded reports whether spent has reached or passed any limit that is set. A zero limit on an axis never triggers, so a run is capped only on the axes its budget actually constrains.

type Spec

type Spec struct {
	Limits Limits `json:"limits"`
}

Spec is a budget's desired state: the ceiling the run may spend up to.

func DecodeSpec

func DecodeSpec(r resource.Resource) (Spec, error)

DecodeSpec reads the typed spec from a resource.

type Spent

type Spent struct {
	Tokens int64   `json:"tokens,omitempty"`
	Cost   float64 `json:"cost,omitempty"`
}

Spent is what a run has consumed so far, accumulated from the metering of every governed action it ran.

func (Spent) IsZero

func (s Spent) IsZero() bool

IsZero reports whether the value consumes nothing on any axis.

type Status

type Status struct {
	Spent    Spent `json:"spent"`
	Reserved Spent `json:"reserved,omitempty"`
}

Status is a budget's observed state: what the run has spent, and what it has reserved but not yet spent. The ledger accumulates both under optimistic concurrency. Reserved is the in-flight commitment a reserve-before-dispatch holds against the pool so concurrent actions (a fan-out of children sharing one budget) admit against one consistent view and cannot all pass an under-budget check and then overshoot together. An action settles its reservation into Spent when it finishes.

func DecodeStatus

func DecodeStatus(r resource.Resource) (Status, error)

DecodeStatus reads the typed status from a resource.

func (Status) Encode

func (s Status) Encode() (json.RawMessage, error)

Encode marshals the status for writing back onto a resource.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL