Class: Nexo::Session

Inherits:
Object
  • Object
show all
Defined in:
lib/nexo/session.rb

Overview

A continuing, addressable session: a remembering instance of an Agent, keyed by (agent_name, instance_id) and resumable across separate invocations. It is the memory half of Nexo's model — the mirror of the fire-and-finish Workflow.

session = Nexo::Session.resume(Assistant, "user-42")
session.prompt("My name is Mac.")
# ... a later request / process ...
Nexo::Session.resume(Assistant, "user-42").prompt("What is my name?")
# => "...Mac..." — the persisted thread carried the earlier turn

A Session adds ONLY memory + addressability. Message persistence is RubyLLM's acts_as_chat (a Session defines no message table and serializes nothing); the agent supplies the sandbox, permissions, skills, MCP servers, and fetch allow-list unchanged. Opening or resuming a session therefore never widens what the agent is allowed to do.

Rails-optional

Durable persistence requires ActiveRecord. Backend selection guards on defined?(::ActiveRecord::Base), mirroring RunStore.default:

  • Rails — the thread is a row on the host acts_as_chat model (Nexo.config.session_chat_model, a String class name, constantized here at resume time so no compile-time AR reference exists). find_or_create_by on (agent_name, instance_id) loads-or-creates it; +acts_as_chat+'s callbacks persist every new message. The host app owns the schema (via rails g ruby_llm:install) plus the two addressing columns and their unique composite index.
  • Plain Ruby — no AR reference at all. A process-wide MemoryStore holds a live RubyLLM::Chat per pair; the thread lives only for the process and is documented as non-durable.

Lifecycle

A long-lived session accumulates context and any MCP/stdio/SSE connections the agent holds. Call #close when done to tear those down; see the README's retention/PII note.

Defined Under Namespace

Classes: MemoryStore

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(agent, instance_id) ⇒ Session

Wraps a built agent and instance_id, then hydrates (finds-or-creates) the persisted thread for the (agent_name, instance_id) pair. Prefer the Session.resume entry point over calling this directly.



58
59
60
61
62
63
# File 'lib/nexo/session.rb', line 58

def initialize(agent, instance_id)
  @agent = agent
  @agent_name = agent.class.name
  @instance_id = instance_id.to_s
  @chat = hydrate
end

Instance Attribute Details

#agent_nameObject (readonly)

The addressing pair for this session: the agent's class name and the caller-supplied instance id.



53
54
55
# File 'lib/nexo/session.rb', line 53

def agent_name
  @agent_name
end

#instance_idObject (readonly)

The addressing pair for this session: the agent's class name and the caller-supplied instance id.



53
54
55
# File 'lib/nexo/session.rb', line 53

def instance_id
  @instance_id
end

Class Method Details

.resume(agent_class, instance_id, **agent_opts) ⇒ Object

Loads-or-creates the persisted thread for (agent_class.name, instance_id) and returns a Session whose #prompt appends to it. Extra keyword arguments are forwarded to agent_class.new so a session accepts the same construction inputs an agent does where they don't conflict with addressing (e.g. cwd:).



47
48
49
# File 'lib/nexo/session.rb', line 47

def self.resume(agent_class, instance_id, **agent_opts)
  new(agent_class.new(**agent_opts), instance_id)
end

Instance Method Details

#closeObject

Releases any MCP/fetch resources the agent holds. Delegates to Agent#close; idempotent and safe when there is nothing to release.



76
77
78
# File 'lib/nexo/session.rb', line 76

def close
  @agent.close if @agent.respond_to?(:close)
end

#prompt(text, max_turns: 25, &on_event) ⇒ Object

Appends text to the thread, runs the agent's loop over the hydrated chat, and returns the response (respond to #content). New messages are persisted by acts_as_chat (Rails) or accumulate on the live in-memory chat (plain Ruby). The optional &on_event block receives the same (:tool_call | :tool_result | :done, payload) events as Loops::RubyLLM.



70
71
72
# File 'lib/nexo/session.rb', line 70

def prompt(text, max_turns: 25, &on_event)
  @agent.loop.run(agent: @agent, prompt: text, max_turns: max_turns, chat: @chat, &on_event)
end