Class: Melete::Runner

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

Overview

Orchestrates Melete — a phantom (non-persisted) LLM loop that observes the main session and prepares skills, workflows, goals, and session names so the main agent can perform cleanly.

Melete’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 management (goal tracking and workflows disabled — sub-agents manage their sole goal via mark_goal_completed)

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

Examples:

Melete::Runner.new(session).call

Defined Under Namespace

Classes: Responsibility

Constant Summary collapse

RESPONSIBILITIES =
{
  session_naming: Responsibility.new(
    prompt: <<~PROMPT,
      ──────────────────────────────
      SESSION NAMING
      ──────────────────────────────
      Name the session once the topic becomes clear. Rename if it shifts.
      Format: one emoji + 1-3 descriptive words.
    PROMPT
    tools: [Tools::RenameSession]
  ),

  sub_agent_naming: Responsibility.new(
    prompt: <<~PROMPT,
      ──────────────────────────────
      SUB-AGENT NAMING
      ──────────────────────────────
      Give this sub-agent a memorable nickname based on its task.
      Format: 1-3 lowercase words joined by hyphens (e.g. "loop-sleuth", "api-scout").
      Evocative, fun, easy to type after @.
      One nickname per call. If taken, pick another — no numeric suffixes.
    PROMPT
    tools: [Tools::AssignNickname]
  ),

  skill_management: Responsibility.new(
    prompt: <<~PROMPT,
      ──────────────────────────────
      SKILL MANAGEMENT
      ──────────────────────────────
      Activate a skill the moment the conversation signals its domain — before Aoide needs it. Late activation means she's working without the knowledge you prepared.

      An activated skill rides Aoide's viewport as a message and leaves on its own when it evicts — you cannot take it back. So be careful: an irrelevant skill crowds her context with text she has to read and ignore until it falls off. Match each activation to the work actually in front of her. Multiple skills can be active at once — each one is a page she has to carry until it evicts.
    PROMPT
    tools: [Tools::ActivateSkill]
  ),

  workflow_management: Responsibility.new(
    prompt: <<~PROMPT,
      ──────────────────────────────
      WORKFLOW MANAGEMENT
      ──────────────────────────────
      Activate a workflow when Aoide starts a multi-step task that matches one. Read the returned content and use judgment to turn it into goals — not a mechanical 1:1 mapping. Adapt: skip irrelevant steps, add extra ones for unfamiliar ground.

      Like skills, a workflow rides Aoide's viewport once activated and leaves when it evicts — there is no deactivation. An irrelevant or stale workflow is text Aoide carries whether she needs it or not, so only activate one when the task genuinely matches.
    PROMPT
    tools: [Tools::ReadWorkflow]
  ),

  goal_tracking: Responsibility.new(
    prompt: <<~PROMPT,
      ──────────────────────────────
      GOAL TRACKING
      ──────────────────────────────
      Create a root goal when Aoide starts a multi-step task. Break it into sub-goals as the plan takes shape. Refine wording as understanding evolves. Mark goals complete when she finishes the work they describe — completing a root cascades through its sub-goals.

      Check the active goals list before every set_goal call. Never duplicate an existing goal — a duplicate wastes a slot and blurs which version Aoide should track.
    PROMPT
    tools: [Tools::SetGoal, Tools::UpdateGoal, Tools::FinishGoal]
  )
}.freeze
BASE_PROMPT =
<<~PROMPT
  You are Melete, the muse of practice. You share the conversation with two sisters — Aoide, who speaks and performs, and Mneme, who holds memory. Your work is preparation: when Aoide speaks, she should have the skills she needs, the workflow in front of her, and a clear sense of what she's working toward.

  Act only through tool calls. Never output text — your contribution is the scene you set, not the words you say.
PROMPT
COMPLETION_PROMPT =
<<~PROMPT
  ──────────────────────────────
  COMPLETION
  ──────────────────────────────
  Finish every run with everything_is_ready. If nothing needs your attention, call it immediately.
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].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)



108
109
110
111
112
113
114
115
# File 'lib/melete/runner.rb', line 108

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

Instance Method Details

#callString?

Runs Melete’s loop. Builds context from the session’s recent messages, 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



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/melete/runner.rb', line 126

def call
  messages = build_messages
  sid = @session.id

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

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

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