ExAgent. Store behaviour
(ExAgent v2.1.0)
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)
save_agent_snapshot/2— persist a snapshot, keyed by itsagent_id.load_agent_snapshot/2— fetch a snapshot byagent_id.list_agent_snapshots/1— list all persisted agent snapshots.delete_agent_snapshot/2— remove a snapshot byagent_id.save_session_snapshot/2/load_session_snapshot/2— the session counterparts used byExAgent.Session.
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
@type agent_snapshot() :: ExAgent.Server.Snapshot.t()
@opaque scoped()
@type session_snapshot() :: ExAgent.Session.Snapshot.t()
Callbacks
@callback capabilities(term()) :: %{ continuation: :atomic | :none, durability: :ephemeral | :durable }
Delete an agent snapshot by agent_id.
Delete a session snapshot by session_id.
@callback list_agent_snapshots(config :: term()) :: [agent_snapshot()]
List every persisted agent snapshot.
@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}.
@callback load_record(term(), record_key()) :: {:ok, map()} | {:error, term()}
@callback load_session_snapshot(config :: term(), session_id :: String.t()) :: {:ok, session_snapshot()} | {:error, :not_found | term()}
Load a session snapshot by session_id.
@callback save_agent_snapshot(config :: term(), snapshot :: agent_snapshot()) :: :ok | {:error, term()}
Persist an agent snapshot, keyed by its agent_id.
@callback save_session_snapshot(config :: term(), snapshot :: session_snapshot()) :: :ok | {:error, term()}
Persist a session snapshot, keyed by session_id.
@callback transition(term(), record_key(), :absent | pos_integer(), map()) :: {:ok, map()} | {:error, term()}
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.
@spec list_agent_snapshots(t()) :: [ExAgent.Server.Snapshot.t()]
List agent snapshots via the resolved tuple.
@spec load_agent_snapshot(t(), String.t()) :: {:ok, ExAgent.Server.Snapshot.t()} | {:error, term()}
Load an agent snapshot via the resolved tuple.
Read the current record using the trusted Store.Scope namespace.
@spec load_session_snapshot(t(), String.t()) :: {:ok, ExAgent.Session.Snapshot.t()} | {:error, term()}
Load a session snapshot via the resolved tuple.
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, []}.
Admit a continuation before effects; callers must explicitly choose durability.
@spec save_agent_snapshot(t(), ExAgent.Server.Snapshot.t()) :: :ok | {:error, term()}
Persist an agent snapshot via the resolved {module, config} tuple.
@spec save_session_snapshot(t(), ExAgent.Session.Snapshot.t()) :: :ok | {:error, term()}
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.
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.
Apply a closed data command atomically. A replayed receipt is not a new effect permit.