Class: Rubino::Agent::Runner
- Inherits:
-
Object
- Object
- Rubino::Agent::Runner
- Defined in:
- lib/rubino/agent/runner.rb
Overview
Top-level orchestrator for a single user interaction. Coordinates session management, the agent loop, and post-turn jobs.
Instance Attribute Summary collapse
-
#model_id ⇒ Object
readonly
The resolved model id this runner runs against.
-
#session ⇒ Object
readonly
Returns the value of attribute session.
Instance Method Summary collapse
-
#cancel! ⇒ Object
Flips the current turn’s cancel token.
-
#end_session! ⇒ Object
Marks the current session ended (#100).
-
#initialize(session_id: nil, model_override: nil, provider_override: nil, max_turns: nil, ignore_rules: false, ui: nil, agent_definition: nil, event_bus: nil, announce_session: true) ⇒ Runner
constructor
A new instance of Runner.
-
#run(input, image_paths: [], input_queue: nil, paste_expansions: []) ⇒ Object
Executes a full interaction turn, swallowing failures so CLI callers can stay in the REPL after a model/tool error.
-
#run!(input, image_paths: [], input_queue: nil, paste_expansions: []) ⇒ Object
Like
runbut propagates exceptions to the caller. -
#switch_model!(model_id) ⇒ Object
Switches the LIVE model for this runner (the in-chat ‘/model <name>`).
Constructor Details
#initialize(session_id: nil, model_override: nil, provider_override: nil, max_turns: nil, ignore_rules: false, ui: nil, agent_definition: nil, event_bus: nil, announce_session: true) ⇒ Runner
Returns a new instance of Runner.
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/rubino/agent/runner.rb', line 14 def initialize(session_id: nil, model_override: nil, provider_override: nil, max_turns: nil, ignore_rules: false, ui: nil, agent_definition: nil, event_bus: nil, announce_session: true) @ui = ui || Rubino.ui # An in-chat rewind/fork builds a runner on the child session but has its # own purpose-built "┄ rewound to message N — editing ┄" marker, so the # generic "Resuming session: <id>…" plumbing line must not also leak into # the transcript (#220). Off-rewind callers keep the announcement. @announce_session = announce_session # Defaults to the process-global bus for the single-run CLI path; the # HTTP Executor injects a fresh per-run bus so concurrent runs don't # cross-contaminate each other's events/output (architecture audit A1). @event_bus = event_bus || Rubino.event_bus @config = Rubino.configuration @session_repo = Session::Repository.new @message_store = Session::Store.new @explicit_model_override = model_override @model_id = model_override || @config.model_default @provider_override = provider_override @max_turns = max_turns @ignore_rules = ignore_rules @agent_definition = agent_definition # Pre-instantiate so cancel! is meaningful between turns and during the # window between Signal.trap install and run() — a too-early Ctrl+C # used to land on a nil token and silently no-op, then the next run # started fresh and the user's cancel was lost. @cancel_token = Interaction::CancelToken.new @session = load_or_create_session(session_id) end |
Instance Attribute Details
#model_id ⇒ Object (readonly)
The resolved model id this runner runs against. Read by SubagentProbe so an ephemeral peek uses the child’s OWN model, not the global default.
12 13 14 |
# File 'lib/rubino/agent/runner.rb', line 12 def model_id @model_id end |
#session ⇒ Object (readonly)
Returns the value of attribute session.
8 9 10 |
# File 'lib/rubino/agent/runner.rb', line 8 def session @session end |
Instance Method Details
#cancel! ⇒ Object
Flips the current turn’s cancel token. Called from the UI thread when the user hits Esc or a second Ctrl+C while the worker is mid-stream. No-op when no turn is in flight.
102 103 104 |
# File 'lib/rubino/agent/runner.rb', line 102 def cancel! @cancel_token&.cancel! end |
#end_session! ⇒ Object
Marks the current session ended (#100). Called from the CLI on a clean REPL teardown (and best-effort on terminal close) so a session stops showing as “active” forever and cleanup/list/–continue can tell a finished session from a live one. Best-effort: a failure here must never crash the exit path.
131 132 133 134 135 136 137 138 139 |
# File 'lib/rubino/agent/runner.rb', line 131 def end_session! # Nothing to end for a session that was never persisted (the user opened # chat and left without sending a message, #144) — there's no row. return if @session.nil? || (@session[:persisted] == false && !@session_repo.persisted?(@session[:id])) @session_repo.end_session!(@session[:id]) rescue StandardError nil end |
#run(input, image_paths: [], input_queue: nil, paste_expansions: []) ⇒ Object
Executes a full interaction turn, swallowing failures so CLI callers can stay in the REPL after a model/tool error. The friendly UI message is emitted, but the bus event INTERACTION_FAILED is NOT re-emitted here — Interaction::Lifecycle is the single source of truth for that, and it already emitted before re-raising. Use run! from non-CLI callers (HTTP executor) that need the exception to propagate so the run row can be marked failed.
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/rubino/agent/runner.rb', line 51 def run(input, image_paths: [], input_queue: nil, paste_expansions: []) run!(input, image_paths: image_paths, input_queue: input_queue, paste_expansions: paste_expansions) rescue Interrupted # Standardized single interrupt notice: a dim `⎿ interrupted` marker # right after the partial answer the Loop already committed via # #stream_end. Replaces the old "⚠ interrupted by user" warning so the # Ctrl+C path and the interrupt-by-default type-ahead path read the same. @ui.turn_interrupted nil rescue SystemExit, Interrupt, SignalException raise rescue Exception => e # rubocop:disable Lint/RescueException @ui.error((e)) nil end |
#run!(input, image_paths: [], input_queue: nil, paste_expansions: []) ⇒ Object
Like run but propagates exceptions to the caller. The HTTP Executor uses this so it can transition the run row to “failed” (instead of mark_completed!) when the lifecycle raises. The ScriptError / Exception net is kept here too so the Executor sees LoadError etc. as a real failure rather than nil-and-completed.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/rubino/agent/runner.rb', line 73 def run!(input, image_paths: [], input_queue: nil, paste_expansions: []) # Each turn gets a fresh token. A CancelToken is one-shot, so reusing a # cancelled one would poison every subsequent turn (it would raise # Interrupted immediately at the first poll point). The per-turn SIGINT # trap (CLI) / stop-watcher (HTTP) is wired to #cancel! against this new # token before any LLM/tool work runs, so an in-flight interrupt still # cancels the current turn. @cancel_token = Interaction::CancelToken.new lifecycle = Interaction::Lifecycle.new( session: @session, event_bus: @event_bus, ui: @ui, config: @config, ignore_rules: @ignore_rules, agent_definition: @agent_definition, cancel_token: @cancel_token, model_override: @explicit_model_override, provider_override: @provider_override, max_tool_iterations: @max_turns ) lifecycle.execute(input, image_paths: image_paths, input_queue: input_queue, paste_expansions: paste_expansions) end |
#switch_model!(model_id) ⇒ Object
Switches the LIVE model for this runner (the in-chat ‘/model <name>`). Lifecycle builds the adapter per turn from `@explicit_model_override || @session`, and the CLI always passes a model_override at boot — so both fields must move for the NEXT turn to actually hit the new model. The session hash is mutated in place (statusbar and /status read it) and the persisted row is updated so resume/–continue agree; an unpersisted lazy session gets the new value via Repository#persist! on its first message instead.
114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/rubino/agent/runner.rb', line 114 def switch_model!(model_id) @explicit_model_override = model_id @model_id = model_id @session[:model] = model_id @session[:provider] = @provider_override || LLM::ProviderResolver.resolve(model_id, explicit_provider: @config.model_provider) if @session_repo.persisted?(@session[:id]) @session_repo.update(@session[:id], model: model_id, provider: @session[:provider]) end model_id end |