Class: Rubino::Interaction::Lifecycle

Inherits:
Object
  • Object
show all
Defined in:
lib/rubino/interaction/lifecycle.rb

Overview

Orchestrates the full lifecycle of a single user interaction. Coordinates all phases from input to final response and post-turn jobs.

Constant Summary collapse

AUX_TITLE_MAX_CHARS =

The longest title the aux summary may produce; matches derive_title’s default truncation so both the aux and deterministic paths yield comparably short titles (#45).

60
PRIORITY_EXTRACT_MEMORY =

Queue priority for the user-visible memory save (#79). Lower = drained first (the queue orders by ‘priority, run_at`). Below the default 100 the other post-turn jobs use, so an ExtractMemoryJob jumps ahead of the SummarizeSessionJob backlog and the “remember X” → recall is prompt.

50

Instance Method Summary collapse

Constructor Details

#initialize(session:, event_bus:, ui:, config:, ignore_rules: false, agent_definition: nil, cancel_token: nil, model_override: nil, provider_override: nil, max_tool_iterations: nil, polishing: nil) ⇒ Lifecycle

Returns a new instance of Lifecycle.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/rubino/interaction/lifecycle.rb', line 28

def initialize(session:, event_bus:, ui:, config:, ignore_rules: false,
               agent_definition: nil, cancel_token: nil,
               model_override: nil, provider_override: nil,
               max_tool_iterations: nil, polishing: nil)
  @session = session
  @event_bus = event_bus
  @ui = ui
  @config = config
  @ignore_rules = ignore_rules
  @agent_definition = agent_definition
  @cancel_token = cancel_token
  @model_override = model_override
  @provider_override = provider_override
  # The Runner-owned detached post-turn polishing worker (#319). When
  # given, the post-turn jobs are handed to it to drain OFF the live
  # turn's critical path so the next prompt is never gated. Nil on the
  # API/server path and nested subagent runs, which keep the original
  # synchronous inline drain (no interactive prompt to free up).
  @polishing = polishing
  # Explicit per-run cap from `--max-turns` (Runner → here → IterationBudget).
  # nil ⇒ use the configured agent_max_tool_iterations (#141).
  @max_tool_iterations = max_tool_iterations
  @session_repo = Session::Repository.new
  @message_store = Session::Store.new
end

Instance Method Details

#active_sessionObject

The session this lifecycle is currently bound to. Starts as the session passed in, but an automatic budget-triggered compaction swaps it to the compaction child (see #check_and_compact). The owning Runner reads this back after #execute so the NEXT turn runs on the (small) child rather than re-compacting the dead parent every turn (P3 F1). Defined as a method (not attr_reader) because @session is REASSIGNED on compaction.



24
25
26
# File 'lib/rubino/interaction/lifecycle.rb', line 24

def active_session
  @session
end

#execute(input, image_paths: [], input_queue: nil, paste_expansions: []) ⇒ Object

Executes the full interaction lifecycle for a user input. image_paths are vision-capable attachments routed natively to the primary model (ruby_llm ‘with:` slot); only consumed on the first iteration of the inner agent loop. Subsequent iterations carry tool results, not user input, and don’t re-attach the images. input_queue is the optional steering hand-off (Interaction::InputQueue) for mid-turn injection: when given, the inner agent loop drains any text the user typed while it was working and folds it into the turn at a safe iteration boundary. Nil for the API/server path and for nested SUBAGENT runs, which stay isolated — no user injection, exactly as before.



64
65
66
67
68
69
70
71
72
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
98
99
100
101
102
103
# File 'lib/rubino/interaction/lifecycle.rb', line 64

def execute(input, image_paths: [], input_queue: nil, paste_expansions: [])
  @event_bus.emit(Events::INTERACTION_STARTED, input: input)

  # 1. Persist user message
  persist_user_message(input, paste_expansions: paste_expansions)

  # 2. Load memory (if enabled)
  memory_context = load_memory(input)

  # 3. Build prompt/context
  messages = build_messages(input, memory_context)
  tools = load_tools

  # 4. Check token budget
  messages = check_and_compact(messages)

  # 5. Run agent loop
  response = run_agent_loop(messages, tools, image_paths: image_paths,
                                             input_queue: input_queue)

  # 6. Persist session state
  update_session_state

  # 7. Enqueue post-turn jobs
  enqueue_post_turn_jobs

  # 8. Finish
  # Carry the final assistant text as the terminal event's authoritative
  # output, regardless of streaming mode. Streaming consumers also receive
  # it incrementally via MODEL_STREAM (message.delta), but the
  # non-streaming path emits no deltas — so without this, a completed run
  # would terminate with no final text for clients to display. This makes
  # run.completed the single source of truth for the answer.
  @event_bus.emit(Events::INTERACTION_FINISHED, output: response.to_s)

  response
rescue StandardError => e
  @event_bus.emit(Events::INTERACTION_FAILED, error: e.message)
  raise
end