Class: ActiveAgent::Delegation::Budget

Inherits:
Object
  • Object
show all
Defined in:
lib/active_agent/delegation/budget.rb

Overview

Cost and latency limits for delegated work.

A sub-agent is a loop inside a loop: the parent model decides how often to call it, and each call spends tokens and wall-clock time nobody explicitly authorized. A budget puts a ceiling on that — per delegation and across the agent as a whole — so a runaway hand-off degrades into a bounded answer instead of an unbounded bill.

Every limit is optional; an empty budget imposes nothing.

Examples:

Per delegation

delegate_to SummarizerAgent, budget: { max_calls: 3, max_tokens: 8_000, timeout: 20 }

Across every delegation this agent makes

delegation_budget max_calls: 10, max_duration: 60, max_cost: 0.25,
                  rates: { input: 0.15, output: 0.60 }

Defined Under Namespace

Classes: Violation

Constant Summary collapse

POLICIES =

What to do when a limit is reached.

:stop — return a structured "budget exhausted" result to the calling model so it can finish with what it already has (default). :raise — raise ActiveAgent::Delegation::BudgetExceededError and abort the generation.

%i[stop raise].freeze
LIMITS =
%i[max_calls max_tokens max_cost max_duration].freeze
KEYS =
(LIMITS + %i[timeout rates on_exceeded]).freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(**options) ⇒ Budget

Returns a new instance of Budget.

Raises:

  • (ArgumentError)


77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/active_agent/delegation/budget.rb', line 77

def initialize(**options)
  unknown = options.keys - KEYS
  raise ArgumentError, "Unknown delegation budget keys: #{unknown.join(", ")}. Valid keys: #{KEYS.join(", ")}" if unknown.any?

  @max_calls    = options[:max_calls]
  @max_tokens   = options[:max_tokens]
  @max_cost     = options[:max_cost]
  @max_duration = options[:max_duration]
  @timeout      = options[:timeout]
  @rates        = options[:rates]
  @on_exceeded  = options[:on_exceeded]&.to_sym

  if @on_exceeded && !POLICIES.include?(@on_exceeded)
    raise ArgumentError, "Unknown delegation budget policy #{@on_exceeded.inspect}. Valid policies: #{POLICIES.join(", ")}"
  end
end

Instance Attribute Details

#max_callsInteger? (readonly)

Returns maximum number of delegated calls.

Returns:

  • (Integer, nil)

    maximum number of delegated calls



48
49
50
# File 'lib/active_agent/delegation/budget.rb', line 48

def max_calls
  @max_calls
end

#max_costFloat? (readonly)

Returns maximum cumulative spend in USD.

Returns:

  • (Float, nil)

    maximum cumulative spend in USD



52
53
54
# File 'lib/active_agent/delegation/budget.rb', line 52

def max_cost
  @max_cost
end

#max_durationFloat? (readonly)

Returns maximum cumulative wall-clock seconds.

Returns:

  • (Float, nil)

    maximum cumulative wall-clock seconds



54
55
56
# File 'lib/active_agent/delegation/budget.rb', line 54

def max_duration
  @max_duration
end

#max_tokensInteger? (readonly)

Returns maximum cumulative tokens across delegated calls.

Returns:

  • (Integer, nil)

    maximum cumulative tokens across delegated calls



50
51
52
# File 'lib/active_agent/delegation/budget.rb', line 50

def max_tokens
  @max_tokens
end

#on_exceededSymbol? (readonly)

Returns :stop or :raise.

Returns:

  • (Symbol, nil)

    :stop or :raise



60
61
62
# File 'lib/active_agent/delegation/budget.rb', line 60

def on_exceeded
  @on_exceeded
end

#ratesHash? (readonly)

Returns inline token rates in USD per 1M tokens.

Returns:

  • (Hash, nil)

    inline token rates in USD per 1M tokens



58
59
60
# File 'lib/active_agent/delegation/budget.rb', line 58

def rates
  @rates
end

#timeoutFloat? (readonly)

Returns per-call wall-clock timeout in seconds.

Returns:

  • (Float, nil)

    per-call wall-clock timeout in seconds



56
57
58
# File 'lib/active_agent/delegation/budget.rb', line 56

def timeout
  @timeout
end

Class Method Details

.build(spec = nil) ⇒ Budget

Coerces a budget spec into a Budget.

Parameters:

  • spec (Budget, Hash, nil) (defaults to: nil)

Returns:

Raises:

  • (ArgumentError)

    on unknown keys or an invalid policy



67
68
69
70
71
72
73
74
75
# File 'lib/active_agent/delegation/budget.rb', line 67

def self.build(spec = nil)
  case spec
  when Budget then spec
  when nil    then new
  when Hash   then new(**spec.symbolize_keys)
  else
    raise ArgumentError, "Delegation budget must be a Hash or #{name}, got #{spec.inspect}"
  end
end

Instance Method Details

#limited?Boolean

Returns whether any limit is set.

Returns:

  • (Boolean)

    whether any limit is set



105
106
107
# File 'lib/active_agent/delegation/budget.rb', line 105

def limited?
  LIMITS.any? { |limit| public_send(limit) }
end

#merge(other) ⇒ Budget

Returns a budget where +other+'s settings win over this one's.

Parameters:

Returns:



98
99
100
101
102
# File 'lib/active_agent/delegation/budget.rb', line 98

def merge(other)
  return self if other.nil?

  self.class.new(**to_h.merge(other.to_h))
end

#policySymbol

Returns the effective policy.

Returns:

  • (Symbol)

    the effective policy



110
111
112
# File 'lib/active_agent/delegation/budget.rb', line 110

def policy
  on_exceeded || :stop
end

#to_hHash

Returns only the settings that were actually provided.

Returns:

  • (Hash)

    only the settings that were actually provided



133
134
135
# File 'lib/active_agent/delegation/budget.rb', line 133

def to_h
  KEYS.index_with { |key| public_send(key) }.compact
end

#violation_for(ledger) ⇒ Violation?

Finds the first limit the ledger has already reached.

Limits are checked before a call runs, because token spend can only be measured after the fact. A budget of max_tokens: 8_000 therefore means "stop delegating once 8,000 tokens have been spent", not "never exceed 8,000 tokens".

Parameters:

Returns:



123
124
125
126
127
128
129
130
# File 'lib/active_agent/delegation/budget.rb', line 123

def violation_for(ledger)
  return Violation.new(limit: :max_calls,    allowed: max_calls,    used: ledger.calls)    if max_calls    && ledger.calls    >= max_calls
  return Violation.new(limit: :max_tokens,   allowed: max_tokens,   used: ledger.tokens)   if max_tokens   && ledger.tokens   >= max_tokens
  return Violation.new(limit: :max_cost,     allowed: max_cost,     used: ledger.cost)     if max_cost     && ledger.cost     >= max_cost
  return Violation.new(limit: :max_duration, allowed: max_duration, used: ledger.duration) if max_duration && ledger.duration >= max_duration

  nil
end