defmodule AgentSea.Memory.Buffer do
@moduledoc """
A simple in-memory conversation buffer backed by a `GenServer`.
Stores a rolling window of messages per conversation id. A single buffer
process serves many conversations (keyed by id); pass `:max_messages` to cap
the window per conversation.
"""
@behaviour AgentSea.Memory
use GenServer
# --- Client ---
@doc "Start a buffer. Options: `:name` (default `#{inspect(__MODULE__)}`), `:max_messages`."
def start_link(opts \\ []) do
name = Keyword.get(opts, :name, __MODULE__)
GenServer.start_link(__MODULE__, opts, name: name)
end
@impl AgentSea.Memory
def save(conversation_id, messages),
do: GenServer.call(__MODULE__, {:save, conversation_id, messages})
@impl AgentSea.Memory
def load(conversation_id),
do: GenServer.call(__MODULE__, {:load, conversation_id})
@impl AgentSea.Memory
def clear(conversation_id),
do: GenServer.call(__MODULE__, {:clear, conversation_id})
@doc "Append messages to a conversation (respecting the window cap)."
def append(conversation_id, messages) when is_list(messages),
do: GenServer.call(__MODULE__, {:append, conversation_id, messages})
# --- Server ---
@impl GenServer
def init(opts) do
{:ok, %{max: Keyword.get(opts, :max_messages), store: %{}}}
end
@impl GenServer
def handle_call({:save, id, messages}, _from, state) do
{:reply, :ok, put(state, id, messages)}
end
def handle_call({:append, id, messages}, _from, state) do
existing = Map.get(state.store, id, [])
{:reply, :ok, put(state, id, existing ++ messages)}
end
def handle_call({:load, id}, _from, state) do
{:reply, Map.get(state.store, id, []), state}
end
def handle_call({:clear, id}, _from, state) do
{:reply, :ok, %{state | store: Map.delete(state.store, id)}}
end
defp put(%{max: max} = state, id, messages) do
windowed = if is_integer(max), do: Enum.take(messages, -max), else: messages
%{state | store: Map.put(state.store, id, windowed)}
end
end