defmodule AgentSea.Memory.Summary do @moduledoc """ Conversation memory that keeps the most recent messages verbatim and compacts everything older into an LLM-generated summary once a conversation grows past a threshold — bounding context size on long chats. `load/1` returns `[summary_system_message | recent_messages]` (or just the messages while under the threshold). Summarization runs over any `AgentSea.Provider`. A singleton (like `AgentSea.Memory.Buffer`): start it once with the provider config. Options: `:provider` (`{module, opts}`, required), `:model`, `:keep_recent` (verbatim tail, default 6), `:threshold` (default 12), `:name`. """ @behaviour AgentSea.Memory use GenServer def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: Keyword.get(opts, :name, __MODULE__)) 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}) # --- Server --- @impl GenServer def init(opts) do {:ok, %{ provider: Keyword.fetch!(opts, :provider), model: Keyword.get(opts, :model), keep: Keyword.get(opts, :keep_recent, 6), threshold: Keyword.get(opts, :threshold, 12), store: %{} }} end @impl GenServer def handle_call({:save, id, messages}, _from, state) do {:reply, :ok, %{state | store: Map.put(state.store, id, compact(messages, state))}} end def handle_call({:load, id}, _from, state) do {:reply, to_messages(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 # --- Compaction --- defp compact(messages, state) do if length(messages) > state.threshold do {older, recent} = Enum.split(messages, length(messages) - state.keep) %{summary: summarize(older, state), recent: recent} else %{summary: nil, recent: messages} end end defp summarize(messages, %{provider: {module, opts}, model: model}) do transcript = Enum.map_join(messages, "\n", fn m -> "#{m.role}: #{m.content}" end) prompt = [ %{ role: :system, content: "Summarize the conversation so far in 2-3 sentences, preserving key facts and decisions." }, %{role: :user, content: transcript} ] case module.complete(prompt, Keyword.put(opts, :model, model)) do {:ok, response} -> response.content {:error, _reason} -> nil end end defp to_messages(nil), do: [] defp to_messages(%{summary: nil, recent: recent}), do: recent defp to_messages(%{summary: summary, recent: recent}) do [%{role: :system, content: "Summary of earlier conversation: " <> summary} | recent] end end