ExAgent.Store behaviour (ExAgent v2.2.1)

View Source

Behaviour for persisting agent/session snapshots and optional atomic records.

ExAgent never owns a database: the store is a pluggable behaviour. The optional ExAgent.Store.ETS keeps snapshots in an in-process ETS table (dev/test); a ExAgent.Store.Postgres uses the application's Repo. Its continuation SQL protocol requires an application-owned Repo, migrations and durable database lifecycle.

Atomic continuation operations are experimental, optional and data-only. They require a trusted scoped/2 descriptor and explicit durability admission. Legacy snapshot operations reject a continuation envelope; legacy lists omit it, preventing an unintegrated runtime from executing its embedded snapshot.

The contract (what a store must do)

Portability rule

Implementations MUST round-trip snapshots through a portable encoding (JSON for the ETS impl), never term_to_binary of arbitrary terms. This guarantees the same data can land in Postgres later without redesign, and refuses to persist non-serializable state (pids, closures). JSON does not redact secrets.

Save replaces one complete snapshot atomically and returns :ok only after the backend confirms the write. Error/timeout can mean an unknown write outcome. There must be one logical writer per key; this contract does not provide distributed locking or replay of external effects. Adapter IO must be bounded: a synchronous blocked adapter also blocks its runtime owner.

A store is referenced as {module, config} where config is opaque to ExAgent (e.g. an ETS table name). normalize/1 turns friendly forms into that tuple; nil means "no store" (the default — one-shot friendly).

Summary

Callbacks

Delete an agent snapshot by agent_id.

Delete a session snapshot by session_id.

List every persisted agent snapshot.

Load an agent snapshot by agent_id, or {:error, :not_found}.

Load a session snapshot by session_id.

Persist an agent snapshot, keyed by its agent_id.

Persist a session snapshot, keyed by session_id.

Functions

Optional atomic capability; unknown or malformed declarations fail closed.

Delete an agent snapshot via the resolved tuple.

Delete a session snapshot via the resolved tuple.

List agent snapshots via the resolved tuple.

Load an agent snapshot via the resolved tuple.

Read the current record using the trusted Store.Scope namespace.

Load a session snapshot via the resolved tuple.

Resolve a store option into a {module, config} tuple, or nil for "no store".

Admit a continuation before effects; callers must explicitly choose durability.

Persist an agent snapshot via the resolved {module, config} tuple.

Persist a session snapshot via the resolved tuple.

Bounded page. Limit bounds physical rows examined, so a page can be empty with a cursor.

Scope snapshot operations to an application-supplied namespace (nonempty UTF-8 string). Nil preserves legacy keys. Use the same namespace on Server/Session and Event topic helpers; never obtain it from untrusted stored data.

Apply a closed data command atomically. A replayed receipt is not a new effect permit.

Types

agent_snapshot()

@type agent_snapshot() :: ExAgent.Server.Snapshot.t()

record_key()

@type record_key() :: {String.t(), :agent | :session, String.t()}

scoped()

@opaque scoped()

session_snapshot()

@type session_snapshot() :: ExAgent.Session.Snapshot.t()

t()

@type t() :: {module(), term()} | scoped()

Callbacks

capabilities(term)

(optional)
@callback capabilities(term()) :: %{
  continuation: :atomic | :none,
  durability: :ephemeral | :durable
}

delete_agent_snapshot(config, agent_id)

@callback delete_agent_snapshot(config :: term(), agent_id :: String.t()) :: :ok

Delete an agent snapshot by agent_id.

delete_session_snapshot(config, session_id)

(optional)
@callback delete_session_snapshot(config :: term(), session_id :: String.t()) :: :ok

Delete a session snapshot by session_id.

list_agent_snapshots(config)

@callback list_agent_snapshots(config :: term()) :: [agent_snapshot()]

List every persisted agent snapshot.

load_agent_snapshot(config, agent_id)

@callback load_agent_snapshot(config :: term(), agent_id :: String.t()) ::
  {:ok, agent_snapshot()} | {:error, :not_found | term()}

Load an agent snapshot by agent_id, or {:error, :not_found}.

load_record(term, record_key)

(optional)
@callback load_record(term(), record_key()) :: {:ok, map()} | {:error, term()}

load_session_snapshot(config, session_id)

(optional)
@callback load_session_snapshot(config :: term(), session_id :: String.t()) ::
  {:ok, session_snapshot()} | {:error, :not_found | term()}

Load a session snapshot by session_id.

save_agent_snapshot(config, snapshot)

@callback save_agent_snapshot(config :: term(), snapshot :: agent_snapshot()) ::
  :ok | {:error, term()}

Persist an agent snapshot, keyed by its agent_id.

save_session_snapshot(config, snapshot)

(optional)
@callback save_session_snapshot(config :: term(), snapshot :: session_snapshot()) ::
  :ok | {:error, term()}

Persist a session snapshot, keyed by session_id.

scan_records(term, t, map)

(optional)
@callback scan_records(term(), String.t(), map()) :: {:ok, map()} | {:error, term()}

transition(term, record_key, arg3, map)

(optional)
@callback transition(term(), record_key(), :absent | pos_integer(), map()) ::
  {:ok, map()} | {:error, term()}

Functions

capabilities(arg1)

Optional atomic capability; unknown or malformed declarations fail closed.

delete_agent_snapshot(scope, agent_id)

@spec delete_agent_snapshot(t(), String.t()) :: :ok | {:error, term()}

Delete an agent snapshot via the resolved tuple.

delete_session_snapshot(scope, session_id)

@spec delete_session_snapshot(t(), String.t()) :: :ok | {:error, term()}

Delete a session snapshot via the resolved tuple.

list_agent_snapshots(scope)

@spec list_agent_snapshots(t()) :: [ExAgent.Server.Snapshot.t()]

List agent snapshots via the resolved tuple.

load_agent_snapshot(scope, agent_id)

@spec load_agent_snapshot(t(), String.t()) ::
  {:ok, ExAgent.Server.Snapshot.t()} | {:error, term()}

Load an agent snapshot via the resolved tuple.

load_record(scope, kind, id)

Read the current record using the trusted Store.Scope namespace.

load_session_snapshot(scope, session_id)

@spec load_session_snapshot(t(), String.t()) ::
  {:ok, ExAgent.Session.Snapshot.t()} | {:error, term()}

Load a session snapshot via the resolved tuple.

normalize(scope)

@spec normalize(term()) :: t() | nil

Resolve a store option into a {module, config} tuple, or nil for "no store".

  • nil → nil (no persistence — default).
  • :ets → {ExAgent.Store.ETS, ExAgent.Store.ETS} (default table).
  • {mod, cfg} → as-is.
  • mod → {mod, []}.

require_continuation(store, durability)

Admit a continuation before effects; callers must explicitly choose durability.

save_agent_snapshot(scope, snapshot)

@spec save_agent_snapshot(t(), ExAgent.Server.Snapshot.t()) :: :ok | {:error, term()}

Persist an agent snapshot via the resolved {module, config} tuple.

save_session_snapshot(scope, snapshot)

@spec save_session_snapshot(t(), ExAgent.Session.Snapshot.t()) ::
  :ok | {:error, term()}

Persist a session snapshot via the resolved tuple.

scan_records(scope, query \\ %{})

Bounded page. Limit bounds physical rows examined, so a page can be empty with a cursor.

scoped(store, namespace)

@spec scoped(term(), String.t() | nil) :: t() | nil

Scope snapshot operations to an application-supplied namespace (nonempty UTF-8 string). Nil preserves legacy keys. Use the same namespace on Server/Session and Event topic helpers; never obtain it from untrusted stored data.

Existing snapshot callbacks receive versioned encoded IDs. Loads verify the exact stored ID before restoring its logical value; no legacy fallback occurs. Lists omit other namespaces and invalid entries. Direct backend calls bypass this dispatch boundary. This provides neither distributed claims nor access control against the application itself.

transition(scope, kind, id, expected, command)

Apply a closed data command atomically. A replayed receipt is not a new effect permit.