Omni.Session.Manager (Omni Agent v0.5.0)

Copy Markdown View Source

Supervises many Omni.Session processes and provides id-based lifecycle management.

The Manager is an app-level Supervisor. Apps define their own module that uses it — following the Ecto.Repo convention — naming the host application via otp_app: and dropping the module into a supervision tree:

defmodule MyApp.Sessions do
  use Omni.Session.Manager, otp_app: :my_app
end

# config/config.exs
config :my_app, MyApp.Sessions,
  store: {Omni.Session.Stores.FileSystem, base_dir: "/var/data/sessions"}

# application.ex
children = [MyApp.Sessions]

Supervisor start-opts override app-env values when both are set, for cases where a value must be computed at boot:

children = [
  {MyApp.Sessions, store: dynamic_store()}
]

Call sites go through the use-generated shorthand:

{:ok, pid}             = MyApp.Sessions.create(agent: [model: {:anthropic, "claude-sonnet-4-5"}])
{:ok, pid, :started}   = MyApp.Sessions.open("abc-123")
:ok                    = MyApp.Sessions.close("abc-123")
{:ok, sessions}        = MyApp.Sessions.list(limit: 50)

What the Manager supervises

MyApp.Sessions (Supervisor, :rest_for_one)
 MyApp.Sessions.Registry (Registry, keys: :unique)
 MyApp.Sessions.DynamicSupervisor (DynamicSupervisor)
 MyApp.Sessions.Tracker (GenServer)
 MyApp.Sessions.TitleService (GenServer, when title_generator  false)

Sessions live under the DynamicSupervisor with restart: :temporary — on crash they do not auto-restart. The Registry maps session ids to pids; on close/2 or crash, entries are removed automatically. The Tracker observes every running session and powers list_open/1 plus the Manager-level subscribe/1 feed.

Running sessions outlive the caller that created them. The caller is auto-subscribed as a :controller by default, so idle-shutdown kicks in once the caller drops off (see :idle_shutdown_after).

Cross-session view

list_open/1 returns a snapshot of all running sessions; each entry is %{id, title, status, pid}. subscribe/1 atomically returns the same snapshot and starts streaming live events to the caller:

{:manager, MyApp.Sessions, :opened, %{id, title, status, pid}}
{:manager, MyApp.Sessions, :status, %{id, status}}
{:manager, MyApp.Sessions, :title,  %{id, title}}
{:manager, MyApp.Sessions, :closed, %{id}}

The second element is the Manager module — what the caller already holds — so subscribers watching multiple Managers route events by pattern-matching.

Configuration

Options resolve in this order (highest priority first): supervisor start-opts → Application.get_env(otp_app, ManagerModule, []) → the defaults below.

  • :storerequired. An Omni.Session.Store adapter — {module, keyword} tuple, bare module, or %Store{} struct. Used by every session this Manager starts, and by list/2 and delete/2. Initialised via Omni.Session.Store.init/1 at supervisor boot.

  • :idle_shutdown_afternil | non_neg_integer(). Default for sessions this Manager starts; overridden per-call. Defaults to 300_000 (5 minutes) when absent. Pass nil to disable Manager-wide.

  • :title_generator — controls automatic title generation for untitled sessions. Accepts:

    • :heuristic (default) — truncates the first user message.
    • {:provider, "model-id"} — uses the given model for LLM-based generation (e.g. {:anthropic, "claude-haiku-4-5"}).
    • {{:provider, "model-id"}, opts} — same, with keyword opts passed through to Omni.generate_text/3 (e.g. :api_key).
    • false — disables auto-titling entirely.

    See Omni.Session.Title for generation details.

  • :name — overrides the registered name. Defaults to the use-ing module.

open/3 return shape

open/3 tells you whether the Manager actually started the session or attached to an already-running one:

{:ok, pid, :started}   # Manager started the process; opts applied
{:ok, pid, :existing}  # process was already up; opts silently dropped

On the :existing branch, start-time opts (:agent, :title, :idle_shutdown_after, :subscribers) are dropped because mutating a live session's configuration safely requires knowing its agent status. Callers who genuinely need fresh config use close/2 + open/3.

:subscribe is honored in both branches — it is a subscription, not a state mutation.

Summary

Types

Per-session entry returned by list_open/1 and subscribe/1.

Functions

Stops a running session. Idempotent — returns :ok if the session is not running. The store is untouched.

Starts a fresh session under this Manager.

Stops the session if running, then deletes it from the store.

Lists sessions from the Manager's store.

Returns the list of sessions currently running under this Manager.

Returns a pid for the session with the given id.

Sets the title of a session by id.

Starts the Manager supervisor and its children.

Subscribes the caller to Manager-level session events.

Unsubscribes the caller from Manager-level events.

Registry lookup — returns the session pid for id, or nil.

Types

entry()

@type entry() :: %{
  id: id(),
  title: String.t() | nil,
  status: :idle | :busy | :paused,
  pid: pid()
}

Per-session entry returned by list_open/1 and subscribe/1.

id()

manager()

@type manager() :: module()

Functions

close(manager, id)

@spec close(manager(), id()) :: :ok

Stops a running session. Idempotent — returns :ok if the session is not running. The store is untouched.

create(manager, opts \\ [])

@spec create(
  manager(),
  keyword()
) ::
  {:ok, pid()}
  | {:error, :already_exists}
  | {:error, {:invalid_opt, atom()}}
  | {:error, term()}

Starts a fresh session under this Manager.

Options:

  • :id — explicit session id (binary). Auto-generated when omitted.
  • :subscribe — boolean, default true. Auto-subscribes the caller as :controller.
  • :agent, :title, :subscribers, :idle_shutdown_after — passed through to Omni.Session.start_link/1.

Rejects Manager-owned opts (:store, :name, :new, :load) with {:error, {:invalid_opt, key}}. Returns {:error, :already_exists} when an explicit :id collides with a running session or one in the store.

delete(manager, id)

@spec delete(manager(), id()) :: :ok | {:error, term()}

Stops the session if running, then deletes it from the store.

Propagates the underlying Omni.Session.Store.delete/3 error.

list(manager, opts \\ [])

@spec list(
  manager(),
  keyword()
) :: {:ok, [Omni.Session.Store.session_info()]}

Lists sessions from the Manager's store.

Pass-through to Omni.Session.Store.list/2. Honours adapter-level opts like :limit and :offset.

list_open(manager)

@spec list_open(manager()) :: [entry()]

Returns the list of sessions currently running under this Manager.

Each entry is a %{id, title, status, pid} map. Ordering is unspecified — callers sort client-side.

Complements list/2 (store-backed, may include sessions that are not running). The two are commonly composed to render an "all sessions with running indicator" view.

open(manager, id, opts \\ [])

@spec open(manager(), id(), keyword()) ::
  {:ok, pid(), :started | :existing}
  | {:error, :not_found}
  | {:error, {:invalid_opt, atom()}}
  | {:error, term()}

Returns a pid for the session with the given id.

The trailing atom tells you what happened:

  • {:ok, pid, :started} — session wasn't running; Manager loaded it from the store, and start-time opts (:agent, :title, :idle_shutdown_after, :subscribers) were applied.
  • {:ok, pid, :existing} — session was already running. Start-time opts are silently dropped (:subscribe still applies).

Returns {:error, :not_found} when no session with the id exists in the store.

The caller is auto-subscribed as :controller by default. Opt out with subscribe: false.

rename(manager, id, title)

@spec rename(manager(), id(), String.t() | nil) :: :ok | {:error, :not_found | term()}

Sets the title of a session by id.

If the session is running, delegates to Omni.Session.set_title/2 — the session handles persistence and event emission as usual.

If the session exists only in the store, updates the title directly and emits a :title event to Manager subscribers.

Returns {:error, :not_found} when the session doesn't exist anywhere (neither running nor persisted).

start_link(opts)

@spec start_link(keyword()) :: Supervisor.on_start()

Starts the Manager supervisor and its children.

Required opt: :name (when called directly without use, pass the module name to register under).

subscribe(manager)

@spec subscribe(manager()) :: {:ok, [entry()]}

Subscribes the caller to Manager-level session events.

Returns an atomic snapshot of currently-running sessions. After the call returns, the caller receives messages of shape:

{:manager, manager_module, :opened, %{id, title, status, pid}}
{:manager, manager_module, :status, %{id, status}}
{:manager, manager_module, :title,  %{id, title}}
{:manager, manager_module, :closed, %{id}}

The second element is the Manager module — the same atom the caller passed in — so a subscriber watching multiple Managers can route events by pattern-matching.

Idempotent per pid: subscribing a second time returns a fresh snapshot without registering duplicate delivery.

unsubscribe(manager)

@spec unsubscribe(manager()) :: :ok

Unsubscribes the caller from Manager-level events.

whereis(manager, id)

@spec whereis(manager(), id()) :: pid() | nil

Registry lookup — returns the session pid for id, or nil.