Class: AnalyticalBrain::Runner

Inherits:
Object
  • Object
show all
Defined in:
lib/analytical_brain/runner.rb

Overview

Orchestrates the analytical brain — a phantom (non-persisted) LLM loop that observes a session and performs background maintenance via tools.

The brain’s capabilities are assembled from independent Responsibility modules, each contributing a prompt section and tools. Which modules are active depends on the session type:

  • **Parent sessions** — session naming, skill/workflow/goal management

  • **Child sessions** — sub-agent nickname assignment, skill/workflow/goal management

Tools mutate the observed session directly (e.g. renaming it, activating skills), but no trace of the brain’s reasoning is persisted — events are emitted into a phantom session (session_id: nil).

Examples:

AnalyticalBrain::Runner.new(session).call

Defined Under Namespace

Classes: Responsibility

Constant Summary collapse

RESPONSIBILITIES =
{
  session_naming: Responsibility.new(
    prompt: <<~PROMPT,
      ──────────────────────────────
      SESSION NAMING
      ──────────────────────────────
      Call rename_session when the topic becomes clear or shifts.
      Format: one emoji + 1-3 descriptive words.
    PROMPT
    tools: [Tools::RenameSession]
  ),

  sub_agent_naming: Responsibility.new(
    prompt: <<~PROMPT,
      ──────────────────────────────
      SUB-AGENT NAMING
      ──────────────────────────────
      Call assign_nickname to give this sub-agent a short, memorable nickname.
      Format: 1-3 lowercase words joined by hyphens (e.g. "loop-sleuth", "api-scout").
      Evocative of the task, fun, easy to type after @.
      Generate EXACTLY ONE nickname. If taken, pick another — no numeric suffixes.
    PROMPT
    tools: [Tools::AssignNickname]
  ),

  skill_management: Responsibility.new(
    prompt: <<~PROMPT,
      ──────────────────────────────
      SKILL MANAGEMENT
      ──────────────────────────────
      Call activate_skill when the conversation matches a skill's description.
      Call deactivate_skill when the agent moves to a different domain.
      Multiple skills can be active at once.
    PROMPT
    tools: [Tools::ActivateSkill, Tools::DeactivateSkill]
  ),

  workflow_management: Responsibility.new(
    prompt: <<~PROMPT,
      ──────────────────────────────
      WORKFLOW MANAGEMENT
      ──────────────────────────────
      Call read_workflow when the user starts a multi-step task matching a workflow description.
      Read the returned content and use judgment to create appropriate goals — not a mechanical 1:1 mapping.
      Adapt to context: skip irrelevant steps, add extra steps for unfamiliar areas.
      Call deactivate_workflow when the workflow completes or the user shifts focus.
      Only one workflow can be active at a time — activating a new one replaces the previous.
    PROMPT
    tools: [Tools::ReadWorkflow, Tools::DeactivateWorkflow]
  ),

  goal_tracking: Responsibility.new(
    prompt: <<~PROMPT,
      ──────────────────────────────
      GOAL TRACKING
      ──────────────────────────────
      Call set_goal to create a root goal when the user starts a multi-step task.
      Call set_goal with parent_goal_id to add sub-goals (TODO items) under it.
      Call update_goal to refine a goal's description as understanding evolves.
      Call finish_goal when the main agent completes work a goal describes.
      Finishing a root goal cascades — all active sub-goals are completed too.
      Never duplicate an existing goal — check the active goals list first.
    PROMPT
    tools: [Tools::SetGoal, Tools::UpdateGoal, Tools::FinishGoal]
  )
}.freeze
BASE_PROMPT =
<<~PROMPT
  You are a background automation that manages session metadata.
  You MUST ONLY communicate through tool calls — NEVER output text.
  Always finish by calling everything_is_ready.
PROMPT
COMPLETION_PROMPT =
<<~PROMPT
  ──────────────────────────────
  COMPLETION
  ──────────────────────────────
  Call everything_is_ready as your LAST tool call, every time.
  If nothing needs changing, call it immediately as your only tool call.
PROMPT
PARENT_RESPONSIBILITIES =

Which responsibilities activate for each session type.

%i[session_naming skill_management workflow_management goal_tracking].freeze
CHILD_RESPONSIBILITIES =
%i[sub_agent_naming skill_management workflow_management goal_tracking].freeze

Instance Method Summary collapse

Constructor Details

#initialize(session, client: nil) ⇒ Runner

Returns a new instance of Runner.

Parameters:

  • session (Session)

    the session to observe and maintain

  • client (LLM::Client, nil) (defaults to: nil)

    injectable LLM client (defaults to fast model)



111
112
113
114
115
116
117
118
# File 'lib/analytical_brain/runner.rb', line 111

def initialize(session, client: nil)
  @session = session
  @client = client || LLM::Client.new(
    model: Anima::Settings.fast_model,
    max_tokens: Anima::Settings.analytical_brain_max_tokens,
    logger: AnalyticalBrain.logger
  )
end

Instance Method Details

#callString?

Runs the analytical brain loop. Builds context from the session’s recent events, calls the LLM with the session-appropriate tool set, and executes any tool calls against the session.

Events emitted during tool execution are not persisted — the phantom session_id (nil) causes the global Persister to skip them.

Returns:

  • (String, nil)

    the LLM’s final text response (discarded by caller), or nil if no context is available



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/analytical_brain/runner.rb', line 129

def call
  messages = build_messages
  sid = @session.id
  if messages.empty?
    log.debug("session=#{sid} — no events, skipping")
    return
  end

  system = build_system_prompt
  log.info("session=#{sid} — running (#{recent_events.size} events)")
  log.debug("system prompt:\n#{system}")
  log.debug("user message:\n#{messages.first[:content]}")

  result = @client.chat_with_tools(
    messages,
    registry: build_registry,
    session_id: nil,
    system: system
  )

  log.info("session=#{sid} — done: #{result.to_s.truncate(200)}")
  result
end