Class: Session

Inherits:
ApplicationRecord show all
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

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.

Parameters:

  • skill_name (String)

    name of the skill to activate

Returns:

Raises:



133
134
135
136
137
138
139
140
141
142
# File 'app/models/session.rb', line 133

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.

Parameters:

  • workflow_name (String)

    name of the workflow to activate

Returns:

Raises:



163
164
165
166
167
168
169
170
171
172
# File 'app/models/session.rb', line 163

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.”

Parameters:

  • environment_context (String, nil) (defaults to: nil)

    pre-assembled environment block

Returns:

  • (String)

    composed system prompt



190
191
192
# File 'app/models/session.rb', line 190

def assemble_system_prompt(environment_context: nil)
  [assemble_soul_section, environment_context, assemble_expertise_section, assemble_goals_section].compact.join("\n\n")
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.

Parameters:

  • skill_name (String)

    name of the skill to deactivate



148
149
150
151
152
153
# File 'app/models/session.rb', line 148

def deactivate_skill(skill_name)
  return unless active_skills.include?(skill_name)

  self.active_skills = active_skills - [skill_name]
  save!
end

#deactivate_workflowvoid

This method returns an undefined value.

Deactivates the current workflow on this session.



177
178
179
180
181
182
# File 'app/models/session.rb', line 177

def deactivate_workflow
  return unless active_workflow.present?

  self.active_workflow = nil
  save!
end

#goals_summaryArray<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.

Returns:

  • (Array<Hash>)

    each with :id, :description, :status, and :sub_goals



199
200
201
# File 'app/models/session.rb', line 199

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. Includes user/agent messages and tool call/response events in Anthropic’s wire format. Consecutive tool_call events are grouped into a single assistant message; consecutive tool_response events are grouped into a single user message with tool_result blocks. Pending messages are excluded — they haven’t been delivered yet.

Parameters:

  • token_budget (Integer) (defaults to: Anima::Settings.token_budget)

    maximum tokens to include (positive)

Returns:

  • (Array<Hash>)

    Anthropic Messages API format



212
213
214
# File 'app/models/session.rb', line 212

def messages_for_llm(token_budget: Anima::Settings.token_budget)
  assemble_messages(viewport_events(token_budget: token_budget, include_pending: false))
end

#next_view_modeString

Cycles to the next view mode: basic → verbose → debug → basic.

Returns:

  • (String)

    the next view mode in the cycle



35
36
37
38
# File 'app/models/session.rb', line 35

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.

Returns:

  • (Integer)

    number of promoted messages



221
222
223
224
225
226
227
228
# File 'app/models/session.rb', line 221

def promote_pending_messages!
  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.

Returns:

  • (Array<Integer>)

    IDs of events no longer in the viewport



95
96
97
98
99
100
101
102
# File 'app/models/session.rb', line 95

def recalculate_viewport!
  new_ids = viewport_events.map(&:id)
  old_ids = viewport_event_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.



53
54
55
56
57
58
59
60
61
62
# File 'app/models/session.rb', line 53

def schedule_analytical_brain!
  return if sub_agent?

  count = events.llm_messages.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

#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).

Parameters:

  • ids (Array<Integer>)

    event IDs now in the viewport



111
112
113
# File 'app/models/session.rb', line 111

def snapshot_viewport!(ids)
  update_column(:viewport_event_ids, ids)
end

#sub_agent?Boolean

Returns true if this session is a sub-agent (has a parent).

Returns:

  • (Boolean)

    true if this session is a sub-agent (has a parent)



41
42
43
# File 'app/models/session.rb', line 41

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.

Parameters:

  • environment_context (String, nil) (defaults to: nil)

    pre-assembled environment block from EnvironmentProbe; injected between soul and expertise sections

Returns:

  • (String, nil)

    the system prompt text, or nil when nothing to inject



122
123
124
# File 'app/models/session.rb', line 122

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.

Parameters:

  • token_budget (Integer) (defaults to: Anima::Settings.token_budget)

    maximum tokens to include (positive)

  • include_pending (Boolean) (defaults to: true)

    whether to include pending messages (true for display, false for LLM context assembly)

Returns:

  • (Array<Event>)

    chronologically ordered



77
78
79
80
81
82
83
84
85
86
87
# File 'app/models/session.rb', line 77

def viewport_events(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