Class: Rubino::Agent::FallbackChain

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

Overview

The provider/model fallback chain — a faithful port of the reference ‘_fallback_chain` + `try_activate_fallback` and the per-turn `_restore_primary_runtime`.

WHAT IT DOES. The primary backend is index 0; ‘agent.fallback_models` lists the ordered fallbacks. When the primary keeps failing — invalid/empty responses (eager fallback), rate-limit/overload, or an exhausted retry budget, or empty-after-retries — the runner / recovery ladder calls #activate_next! to rotate to the next backend and rebuild the adapter. At the TOP of each new turn ConversationLoop#run calls #restore_primary! so every turn gets a fresh attempt with the preferred model.

DEDUP. An entry that resolves to the CURRENT provider/model/base_url is skipped — falling back to the backend that just failed only loops the failure. We keep advancing past skipped entries in a single #activate_next! call, exactly like the reference recursive ‘return agent._try_activate_fallback()`.

GLOBAL-CONFIG ISOLATION (the heart of this slice). ‘RubyLLM.configure` is process-global; a naive provider swap would corrupt concurrent sessions on the API/server path. So fallback adapters are built with `isolate_config: true`: each scopes its provider config (base_url / api_key / timeout) into a per-adapter `RubyLLM::Context` and NEVER writes the global. The primary adapter is passed in as-is (it already configured the global at construction, exactly as before), so a single-provider setup — and the no-fallback case — is byte-identical to pre-Slice-7 behaviour.

NO-OP WHEN UNCONFIGURED. With an empty ‘fallback_models` the chain holds only the primary: #activate_next! is always false and #current_adapter is always the primary. Nothing is rebuilt, nothing is mutated.

Defined Under Namespace

Classes: Entry

Instance Method Summary collapse

Constructor Details

#initialize(primary_adapter:, config:, ui: nil, event_bus: nil, tool_executor: nil, cancel_token: nil, adapter_builder: LLM::AdapterFactory) ⇒ FallbackChain

primary_adapter : the already-built primary LLM adapter (index 0). The

chain never rebuilds it — restore just points back to it.

config : the live Configuration (reads agent.fallback_models and

the providers.* blocks the fallback entries inherit).

adapter_builder : injectable seam for tests; defaults to AdapterFactory.



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/rubino/agent/fallback_chain.rb', line 50

def initialize(primary_adapter:, config:, ui: nil, event_bus: nil,
               tool_executor: nil, cancel_token: nil,
               adapter_builder: LLM::AdapterFactory)
  @primary         = primary_adapter
  @config          = config
  @ui              = ui
  @event_bus       = event_bus
  @tool_executor   = tool_executor
  @cancel_token    = cancel_token
  @adapter_builder = adapter_builder

  @entries = build_entries
  @index   = 0
  @active  = @primary
end

Instance Method Details

#activate_next!Object

Advance to the next usable, non-duplicate fallback and rebuild the adapter. Returns true if it actually switched, false when the chain is exhausted (or empty). Mirrors try_activate_fallback (helpers.py:1020): skip invalid entries and entries that resolve to the current backend, advancing past them within this one call.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/rubino/agent/fallback_chain.rb', line 82

def activate_next!
  loop do
    return false if @index >= @entries.size

    entry = @entries[@index]
    @index += 1

    next unless entry.usable?
    next if duplicate_of_current?(entry)

    @active = build_adapter(entry)
    return true
  end
end

#active?Boolean

True once a fallback has been activated this turn — lets callers emit the “switched to fallback” status only when something actually changed.

Returns:

  • (Boolean)


73
74
75
# File 'lib/rubino/agent/fallback_chain.rb', line 73

def active?
  @index.positive?
end

#current_adapterObject

The adapter the loop/runner should issue calls against right now.



67
68
69
# File 'lib/rubino/agent/fallback_chain.rb', line 67

def current_adapter
  @active
end

#restore_primary!Object

Reset to the primary at the top of each turn. No-op cost when we never left the primary; rebuilds nothing (the primary adapter is the one handed in at construction).



100
101
102
103
# File 'lib/rubino/agent/fallback_chain.rb', line 100

def restore_primary!
  @index  = 0
  @active = @primary
end