Planck.Agent.Session (Planck.Agent v0.1.0)

Copy Markdown View Source

Persistent session store backed by SQLite.

One GenServer per session, registered globally so any node in the cluster can append messages or query history via transparent GenServer calls. Each session writes to <dir>/<id>_<name>.db.

Both id and name appear in the filename so either can be resolved with a single directory glob — see find_by_id/2 and find_by_name/2.

Usage

{:ok, _pid} = Planck.Agent.Session.start("a1b2c3d4", name: "crazy-mango", dir: "/path/to/sessions")

:ok = Planck.Agent.Session.append("my-session", "agent-1", message)

{:ok, rows} = Planck.Agent.Session.messages("my-session")
{:ok, rows} = Planck.Agent.Session.messages("my-session", agent_id: "agent-1")

Each row is %{db_id: pos_integer(), agent_id: String.t(), message: Message.t(), inserted_at: integer()}. db_id is the SQLite autoincrement row id — use it with truncate_after/2 to anchor a truncation to a specific message.

Messages are serialized with :erlang.term_to_binary/1 and read back with :erlang.binary_to_term/2 (:safe — no new atoms created from DB content).

start/2 requires an explicit :dir option — the sessions directory is resolved by the caller (typically Planck.Headless from its config).

Distribution

Sessions are registered via :global as {:session, session_id}. Any node in the Erlang cluster can call append/3 or messages/2 — the call is routed transparently to the node that owns the session's SQLite file.

Pagination

Messages with role {:custom, :summary} are stored as checkpoints (checkpoint = 1 in the DB). Two functions support cursor-based pagination anchored on these checkpoints:

Pass the returned checkpoint_id integer back as the cursor for the next page.

Summary

Types

A row returned by messages/2 and related query functions.

Functions

Append a message and return its DB row id. Returns nil if the session is not found (agent has no persistent session).

Resolve a session file by id. Globs <sessions_dir>/<id>_*.db.

Resolve a session file by name. Globs <sessions_dir>/*_<name>.db.

Return all metadata for a session as a %{String.t() => String.t() | nil} map.

Retrieve messages for a session in insertion order.

Return the chapter before a given checkpoint: the previous summary checkpoint and all messages between it and checkpoint_id.

Return the latest summary checkpoint and all messages after it.

Write key-value metadata for a session. Merges with any existing entries; existing keys are overwritten. Values are stored as strings.

Start a session under the SessionSupervisor.

Stop a running session.

Delete all messages with a DB row id >= db_id, across all agents in the session.

Resolve a session id to its pid via :global.

Types

row()

@type row() :: %{
  db_id: pos_integer(),
  agent_id: String.t(),
  message: Planck.Agent.Message.t(),
  inserted_at: integer()
}

A row returned by messages/2 and related query functions.

session_id()

@type session_id() :: String.t()

Functions

append(session_id, agent_id, message)

@spec append(session_id(), String.t(), Planck.Agent.Message.t()) ::
  pos_integer() | nil

Append a message and return its DB row id. Returns nil if the session is not found (agent has no persistent session).

find_by_id(sessions_dir, session_id)

@spec find_by_id(Path.t(), String.t()) ::
  {:ok, Path.t(), String.t()} | {:error, :not_found}

Resolve a session file by id. Globs <sessions_dir>/<id>_*.db.

Returns {:ok, path, name} or {:error, :not_found}.

find_by_name(sessions_dir, name)

@spec find_by_name(Path.t(), String.t()) ::
  {:ok, Path.t(), String.t()} | {:error, :not_found}

Resolve a session file by name. Globs <sessions_dir>/*_<name>.db.

Returns {:ok, path, session_id} or {:error, :not_found}.

get_metadata(session_id)

@spec get_metadata(session_id()) ::
  {:ok, %{optional(String.t()) => String.t() | nil}} | {:error, :not_found}

Return all metadata for a session as a %{String.t() => String.t() | nil} map.

messages(session_id, opts \\ [])

@spec messages(
  session_id(),
  keyword()
) :: {:ok, [row()]} | {:error, :not_found}

Retrieve messages for a session in insertion order.

Options:

  • agent_id: — filter to messages from a specific agent

messages_before_checkpoint(session_id, checkpoint_id, opts \\ [])

@spec messages_before_checkpoint(session_id(), non_neg_integer(), keyword()) ::
  {:ok, [row()], non_neg_integer() | nil} | {:error, :not_found}

Return the chapter before a given checkpoint: the previous summary checkpoint and all messages between it and checkpoint_id.

Returns {:ok, rows, prev_checkpoint_id | nil}. When prev_checkpoint_id is nil there is no further history to load.

Options:

  • agent_id: — filter to a specific agent

messages_from_latest_checkpoint(session_id, opts \\ [])

@spec messages_from_latest_checkpoint(
  session_id(),
  keyword()
) :: {:ok, [row()], non_neg_integer() | nil} | {:error, :not_found}

Return the latest summary checkpoint and all messages after it.

If no checkpoint exists, returns all messages from the beginning. The checkpoint_id in the return tuple is the DB row id of the checkpoint — pass it to messages_before_checkpoint/3 to load the previous page.

Options:

  • agent_id: — filter to a specific agent

save_metadata(session_id, metadata)

@spec save_metadata(session_id(), map()) :: :ok | {:error, :not_found}

Write key-value metadata for a session. Merges with any existing entries; existing keys are overwritten. Values are stored as strings.

start(session_id, opts \\ [])

@spec start(
  session_id(),
  keyword()
) :: {:ok, pid()} | {:error, term()}

Start a session under the SessionSupervisor.

stop(session_id)

@spec stop(session_id()) :: :ok | {:error, :not_found | term()}

Stop a running session.

truncate_after(session_id, db_id)

@spec truncate_after(session_id(), pos_integer()) :: :ok | {:error, :not_found}

Delete all messages with a DB row id >= db_id, across all agents in the session.

Used when editing a previous message: truncates the session to strictly before the given row, then the caller re-prompts with new text.

whereis(session_id)

@spec whereis(session_id()) :: {:ok, pid()} | {:error, :not_found}

Resolve a session id to its pid via :global.