Class: Session
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Session
- Defined in:
- app/models/session.rb
Overview
A conversation session — the fundamental unit of agent interaction. Owns an ordered stream of Event records representing everything that happened: user messages, agent responses, tool calls, etc.
Sessions form a hierarchy: a main session can spawn child sessions (sub-agents) that inherit the parent’s viewport context at fork time.
Defined Under Namespace
Classes: MissingSoulError
Constant Summary collapse
- VIEW_MODES =
%w[basic verbose debug].freeze
Instance Method Summary collapse
-
#activate_skill(skill_name) ⇒ Skills::Definition
Activates a skill on this session.
-
#activate_workflow(workflow_name) ⇒ Workflows::Definition
Activates a workflow on this session.
-
#assemble_system_prompt(environment_context: nil) ⇒ String
Assembles the system prompt: soul first, then environment context, then skills/workflow, then goals.
-
#broadcast_children_update_to_parent ⇒ void
Broadcasts child session list to all clients subscribed to the parent session.
-
#create_user_event(content) ⇒ Event
Creates a user message event record directly (bypasses EventBus+Persister).
-
#deactivate_skill(skill_name) ⇒ void
Deactivates a skill on this session.
-
#deactivate_workflow ⇒ void
Deactivates the current workflow on this session.
-
#goals_summary ⇒ Array<Hash>
Serializes active goals as a lightweight summary for ActionCable broadcasts and TUI display.
-
#messages_for_llm(token_budget: Anima::Settings.token_budget) ⇒ Array<Hash>
Builds the message array expected by the Anthropic Messages API.
-
#next_view_mode ⇒ String
Cycles to the next view mode: basic → verbose → debug → basic.
-
#promote_pending_messages! ⇒ Integer
Promotes all pending user messages to delivered status so they appear in the next LLM context.
-
#recalculate_viewport! ⇒ Array<Integer>
Recalculates the viewport and returns IDs of events evicted since the last snapshot.
-
#schedule_analytical_brain! ⇒ void
Enqueues the analytical brain to perform background maintenance on this session.
-
#schedule_mneme! ⇒ void
Checks whether the Mneme terminal event has left the viewport and enqueues MnemeJob when it has.
-
#snapshot_viewport!(ids) ⇒ void
Overwrites the viewport snapshot without computing evictions.
-
#sub_agent? ⇒ Boolean
True if this session is a sub-agent (has a parent).
-
#system_prompt(environment_context: nil) ⇒ String?
Returns the system prompt for this session.
-
#viewport_events(token_budget: Anima::Settings.token_budget, include_pending: true) ⇒ Array<Event>
Returns the events currently visible in the LLM context window.
Instance Method Details
#activate_skill(skill_name) ⇒ Skills::Definition
Activates a skill on this session. Validates the skill exists in the registry, adds it to active_skills, and persists.
167 168 169 170 171 172 173 174 175 176 |
# File 'app/models/session.rb', line 167 def activate_skill(skill_name) definition = Skills::Registry.instance.find(skill_name) raise Skills::InvalidDefinitionError, "Unknown skill: #{skill_name}" unless definition return definition if active_skills.include?(skill_name) self.active_skills = active_skills + [skill_name] save! definition end |
#activate_workflow(workflow_name) ⇒ Workflows::Definition
Activates a workflow on this session. Validates the workflow exists in the registry, sets it as the active workflow, and persists. Only one workflow can be active at a time — activating a new one replaces the previous.
197 198 199 200 201 202 203 204 205 206 |
# File 'app/models/session.rb', line 197 def activate_workflow(workflow_name) definition = Workflows::Registry.instance.find(workflow_name) raise Workflows::InvalidDefinitionError, "Unknown workflow: #{workflow_name}" unless definition return definition if active_workflow == workflow_name self.active_workflow = workflow_name save! definition end |
#assemble_system_prompt(environment_context: nil) ⇒ String
Assembles the system prompt: soul first, then environment context, then skills/workflow, then goals. The soul is always present — “who am I” before “what can I do.”
224 225 226 |
# File 'app/models/session.rb', line 224 def assemble_system_prompt(environment_context: nil) [assemble_soul_section, environment_context, assemble_expertise_section, assemble_goals_section].compact.join("\n\n") end |
#broadcast_children_update_to_parent ⇒ void
This method returns an undefined value.
Broadcasts child session list to all clients subscribed to the parent session. Called when a child session is created or its processing state changes so the HUD sub-agents section updates in real time.
Queries children via FK directly (avoids loading the parent record) and selects only the columns needed for the HUD payload.
318 319 320 321 322 323 324 325 326 327 328 329 |
# File 'app/models/session.rb', line 318 def broadcast_children_update_to_parent return unless parent_session_id children = Session.where(parent_session_id: parent_session_id) .order(:created_at) .select(:id, :name, :processing) ActionCable.server.broadcast("session_#{parent_session_id}", { "action" => "children_updated", "session_id" => parent_session_id, "children" => children.map { |child| {"id" => child.id, "name" => child.name, "processing" => child.processing?} } }) end |
#create_user_event(content) ⇒ Event
Creates a user message event record directly (bypasses EventBus+Persister). Used by AgentRequestJob (Bounce Back transaction), AgentLoop#process, and sub-agent spawn tools (Tools::SpawnSubagent, Tools::SpawnSpecialist) because the global Events::Subscribers::Persister skips non-pending user messages — these callers own the persistence lifecycle.
287 288 289 290 291 292 293 294 |
# File 'app/models/session.rb', line 287 def create_user_event(content) now = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond) events.create!( event_type: "user_message", payload: {type: "user_message", content: content, session_id: id, timestamp: now}, timestamp: now ) end |
#deactivate_skill(skill_name) ⇒ void
This method returns an undefined value.
Deactivates a skill on this session. Removes it from active_skills and persists.
182 183 184 185 186 187 |
# File 'app/models/session.rb', line 182 def deactivate_skill(skill_name) return unless active_skills.include?(skill_name) self.active_skills = active_skills - [skill_name] save! end |
#deactivate_workflow ⇒ void
This method returns an undefined value.
Deactivates the current workflow on this session.
211 212 213 214 215 216 |
# File 'app/models/session.rb', line 211 def deactivate_workflow return unless active_workflow.present? self.active_workflow = nil save! end |
#goals_summary ⇒ Array<Hash>
Serializes active goals as a lightweight summary for ActionCable broadcasts and TUI display. Returns a nested structure: root goals with their sub-goals inlined.
233 234 235 |
# File 'app/models/session.rb', line 233 def goals_summary goals.root.includes(:sub_goals).order(:created_at).map(&:as_summary) end |
#messages_for_llm(token_budget: Anima::Settings.token_budget) ⇒ Array<Hash>
Builds the message array expected by the Anthropic Messages API. Viewport layout (top to bottom):
[L2 snapshots] [L1 snapshots] [pinned events] [recalled memories] [sliding window events]
Snapshots appear ONLY after their source events have evicted from the sliding window. L1 snapshots drop once covered by an L2 snapshot. Pinned events are critical context attached to active Goals — they survive eviction intact until their Goals complete. Recalled memories surface relevant older events (passive recall via goals). Each layer has a fixed token budget fraction — snapshots, pins, and recall consume viewport space, reducing the sliding window size.
Sub-agent sessions skip snapshot/pin/recall injection (they inherit parent events directly).
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 |
# File 'app/models/session.rb', line 253 def (token_budget: Anima::Settings.token_budget) sliding_budget = token_budget = [] = [] = [] unless sub_agent? l2_budget = (token_budget * Anima::Settings.mneme_l2_budget_fraction).to_i l1_budget = (token_budget * Anima::Settings.mneme_l1_budget_fraction).to_i pinned_budget = (token_budget * Anima::Settings.mneme_pinned_budget_fraction).to_i recall_budget = (token_budget * Anima::Settings.recall_budget_fraction).to_i sliding_budget = token_budget - l2_budget - l1_budget - pinned_budget - recall_budget end events = (token_budget: sliding_budget, include_pending: false) unless sub_agent? first_event_id = events.first&.id = (first_event_id, l2_budget: l2_budget, l1_budget: l1_budget) = (first_event_id, budget: pinned_budget) = (budget: recall_budget) end + + + (events) end |
#next_view_mode ⇒ String
Cycles to the next view mode: basic → verbose → debug → basic.
37 38 39 40 |
# File 'app/models/session.rb', line 37 def next_view_mode current_index = VIEW_MODES.index(view_mode) || 0 VIEW_MODES[(current_index + 1) % VIEW_MODES.size] end |
#promote_pending_messages! ⇒ Integer
Promotes all pending user messages to delivered status so they appear in the next LLM context. Triggers broadcast_update for each event so connected clients refresh the pending indicator.
301 302 303 304 305 306 307 308 |
# File 'app/models/session.rb', line 301 def promoted = 0 events.where(event_type: "user_message", status: Event::PENDING_STATUS).find_each do |event| event.update!(status: nil, payload: event.payload.except("status")) promoted += 1 end promoted end |
#recalculate_viewport! ⇒ Array<Integer>
Recalculates the viewport and returns IDs of events evicted since the last snapshot. Updates the stored viewport_event_ids atomically. Piggybacks on event broadcasts to notify clients which messages left the LLM’s context window.
129 130 131 132 133 134 135 136 |
# File 'app/models/session.rb', line 129 def new_ids = .map(&:id) old_ids = evicted = old_ids - new_ids update_column(:viewport_event_ids, new_ids) if old_ids != new_ids evicted end |
#schedule_analytical_brain! ⇒ void
This method returns an undefined value.
Enqueues the analytical brain to perform background maintenance on this session. Currently handles session naming; future phases add skill activation, goal tracking, and memory.
Runs after the first exchange and periodically as the conversation evolves, so the name stays relevant to the current topic.
87 88 89 90 91 92 93 94 95 96 |
# File 'app/models/session.rb', line 87 def schedule_analytical_brain! return if sub_agent? count = events..count return if count < 2 # Already named — only regenerate at interval boundaries (30, 60, 90, …) return if name.present? && (count % Anima::Settings.name_generation_interval != 0) AnalyticalBrainJob.perform_later(id) end |
#schedule_mneme! ⇒ void
This method returns an undefined value.
Checks whether the Mneme terminal event has left the viewport and enqueues MnemeJob when it has. On the first event of a new session, initializes the boundary pointer.
The terminal event is always a conversation event (user/agent message or think tool_call), never a bare tool_call/tool_response.
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'app/models/session.rb', line 55 def schedule_mneme! return if sub_agent? # Initialize boundary on first conversation event if mneme_boundary_event_id.nil? first_conversation = events.deliverable .where(event_type: Event::CONVERSATION_TYPES) .order(:id).first first_conversation ||= events.deliverable .where(event_type: "tool_call") .detect { |e| e.payload["tool_name"] == Event::THINK_TOOL } if first_conversation update_column(:mneme_boundary_event_id, first_conversation.id) end return end # Check if boundary event has left the viewport return if .include?(mneme_boundary_event_id) MnemeJob.perform_later(id) end |
#snapshot_viewport!(ids) ⇒ void
This method returns an undefined value.
Overwrites the viewport snapshot without computing evictions. Used when transmitting or broadcasting a full viewport refresh, where eviction notifications are unnecessary (clients clear their store first).
145 146 147 |
# File 'app/models/session.rb', line 145 def (ids) update_column(:viewport_event_ids, ids) end |
#sub_agent? ⇒ Boolean
Returns true if this session is a sub-agent (has a parent).
43 44 45 |
# File 'app/models/session.rb', line 43 def sub_agent? parent_session_id.present? end |
#system_prompt(environment_context: nil) ⇒ String?
Returns the system prompt for this session. Sub-agent sessions use their stored prompt. Main sessions assemble a system prompt from active skills and current goals.
156 157 158 |
# File 'app/models/session.rb', line 156 def system_prompt(environment_context: nil) sub_agent? ? prompt : assemble_system_prompt(environment_context: environment_context) end |
#viewport_events(token_budget: Anima::Settings.token_budget, include_pending: true) ⇒ Array<Event>
Returns the events currently visible in the LLM context window. Walks events newest-first and includes them until the token budget is exhausted. Events are full-size or excluded entirely.
Sub-agent sessions inherit parent context via virtual viewport: child events are prioritized and fill the budget first (newest-first), then parent events from before the fork point fill the remaining budget. The final array is chronological: parent events first, then child events.
111 112 113 114 115 116 117 118 119 120 121 |
# File 'app/models/session.rb', line 111 def (token_budget: Anima::Settings.token_budget, include_pending: true) own_events = select_events(own_event_scope(include_pending), budget: token_budget) remaining = token_budget - own_events.sum { |e| event_token_cost(e) } if sub_agent? && remaining > 0 parent_events = select_events(parent_event_scope(include_pending), budget: remaining) trim_trailing_tool_calls(parent_events) + own_events else own_events end end |