Current section

Files

Jump to
nous lib nous persistence ets.ex
Raw

lib/nous/persistence/ets.ex

defmodule Nous.Persistence.ETS do
@moduledoc """
ETS-based persistence backend.
Stores serialized context data in a named ETS table. The table is owned
by a dedicated GenServer started under the Nous application supervisor,
so the table outlives transient callers - previously the table died
with whichever process happened to call save/load first.
Data does not survive node restarts. Useful for development, testing,
and short-lived sessions.
## Usage
# In AgentServer config
AgentServer.start_link(
session_id: "session_123",
agent_config: %{model: "openai:gpt-4", instructions: "Be helpful"},
persistence: Nous.Persistence.ETS
)
"""
@behaviour Nous.Persistence
@table :nous_persistence
defmodule TableOwner do
@moduledoc false
use GenServer
def start_link(_opts) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
@impl true
def init(:ok) do
table_name = :nous_persistence
table =
case :ets.whereis(table_name) do
:undefined ->
:ets.new(table_name, [:named_table, :set, :public, read_concurrency: true])
_ref ->
table_name
end
{:ok, %{table: table}}
end
end
@doc false
def child_spec(_opts) do
%{id: __MODULE__, start: {TableOwner, :start_link, [[]]}, type: :worker}
end
@impl true
def save(session_id, data) when is_binary(session_id) and is_map(data) do
ensure_table()
try do
true = :ets.insert(@table, {session_id, data})
:ok
rescue
e -> {:error, {:ets_insert_failed, Exception.message(e)}}
end
end
@impl true
def load(session_id) when is_binary(session_id) do
ensure_table()
case :ets.lookup(@table, session_id) do
[{^session_id, data}] -> {:ok, data}
[] -> {:error, :not_found}
end
end
@impl true
def delete(session_id) when is_binary(session_id) do
ensure_table()
:ets.delete(@table, session_id)
:ok
end
@impl true
def list do
ensure_table()
keys = :ets.foldl(fn {key, _val}, acc -> [key | acc] end, [], @table)
{:ok, keys}
end
# Fallback for callers used before the supervisor started the owner
# (mainly ad-hoc tests). Production code goes through TableOwner.
defp ensure_table do
case :ets.whereis(@table) do
:undefined ->
try do
:ets.new(@table, [:named_table, :set, :public, read_concurrency: true])
rescue
ArgumentError ->
# Another process created the table between whereis and new
:ok
end
_ref ->
:ok
end
end
end