Class: Rubino::Agent::IterationBudget

Inherits:
Object
  • Object
show all
Defined in:
lib/rubino/agent/iteration_budget.rb

Overview

Manages turn and iteration budgets to prevent runaway loops.

Constant Summary collapse

MAX_CAP =

Upper bound on a turn/iteration cap (F2). A 0/negative cap is rejected as “would never run”; a value this large is just as nonsensical the other way — ‘–max-turns 99999999999999` was silently accepted, defeating the whole point of a runaway guard. Reject anything above this sane ceiling with the same clear message rather than letting an effectively-unbounded cap through. 10_000 is far past any legitimate agentic turn yet small enough to keep the comparison meaningful.

10_000

Instance Method Summary collapse

Constructor Details

#initialize(config: nil, max_tool_iterations: nil) ⇒ IterationBudget

Returns a new instance of IterationBudget.



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/rubino/agent/iteration_budget.rb', line 16

def initialize(config: nil, max_tool_iterations: nil)
  @config = config || Rubino.configuration
  # A 0/negative cap is nonsense (the turn could never run a single
  # iteration), so REJECT it with a clear message at both entry points —
  # the configured `agent.max_turns` and the CLI `--max-turns N` override —
  # rather than silently coercing it to "unbounded" / the default and
  # surprising the user. nil/absent stays meaningful (unbounded rail /
  # config default).
  @max_turns = require_positive_cap!(@config.agent_max_turns, "agent.max_turns")
  # An explicit override (the CLI `--max-turns N` flag, threaded through
  # Runner → Lifecycle) wins over the config default so the documented
  # control knob actually caps tool iterations (#141). A nil/blank
  # override falls back to the configured budget, unchanged.
  override = require_positive_cap!(max_tool_iterations, "--max-turns")
  @max_tool_iterations = override || @config.agent_max_tool_iterations
  @max_turn_seconds = @config.agent_max_turn_seconds
  @turn_started_at = Time.now
end

Instance Method Details

#can_continue?(iteration) ⇒ Boolean

Returns true if the agent can continue iterating

Returns:

  • (Boolean)


36
37
38
# File 'lib/rubino/agent/iteration_budget.rb', line 36

def can_continue?(iteration)
  within_iteration_limit?(iteration) && within_time_limit?
end

#extend!(by) ⇒ Object

Grants ‘by` more tool iterations so a turn that hit the cap can resume the SAME turn with full context (#399, the Cline/Roo “reset the counter, keep context” pattern). Only the soft iteration ceiling moves — the max_turns OUTER rail and the max_turn_seconds safety-net are untouched, so repeated extensions can never bypass the max_turns/clock ceiling (a runaway still stops at max_turns). No-op on an unbounded (nil) cap. Returns the new ceiling.



61
62
63
64
65
66
# File 'lib/rubino/agent/iteration_budget.rb', line 61

def extend!(by)
  amount = positive_int(by)
  return @max_tool_iterations if amount.nil? || @max_tool_iterations.nil?

  @max_tool_iterations += amount
end

#extendable?(iteration) ⇒ Boolean

True ONLY when offering the interactive Continue extension would actually help: the SOFT iteration ceiling (@max_tool_iterations) is what’s exhausted, and neither non-extendable rail is the blocker (#403). extend! raises only the soft ceiling, so it is impotent against the TIME limit AND the max_turns OUTER rail. When either of those is what’s spent, extending is a no-op and re-prompting would loop forever — callers must force-summarize instead. Hence extendable? is FALSE when the time limit OR the max_turns outer rail is the blocker, and only TRUE when the soft iteration ceiling is what’s exhausted. Also false on an unbounded soft cap (nothing to extend).

Returns:

  • (Boolean)


50
51
52
# File 'lib/rubino/agent/iteration_budget.rb', line 50

def extendable?(iteration)
  within_time_limit? && within_turns_rail?(iteration) && !within_soft_iteration_limit?(iteration)
end