guides/sessions_and_persistence.md

# Sessions and Persistence

Every running agent owns a `Condukt.Session`: the `GenServer` that holds
conversation history, tool configuration, and runtime options. By default
sessions live only for the lifetime of the process. A session store lets you
snapshot and restore them.

## Built-in stores

* `Condukt.SessionStore.Memory` keeps snapshots in ETS. Useful for restoring
  state within a running BEAM (for example after a `GenServer` crash).
* `Condukt.SessionStore.Disk` writes snapshots to disk. Useful for crashing
  recovery, deployments, and CLIs.

```elixir
# Restore within the current VM
{:ok, agent} =
  MyApp.CodingAgent.start_link(
    session_store:
      {Condukt.SessionStore.Memory, key: {:coding_agent, "/tmp/project"}}
  )

# Persist to disk across restarts
{:ok, agent} =
  MyApp.CodingAgent.start_link(
    cwd: "/tmp/project",
    session_store: Condukt.SessionStore.Disk
  )

# Custom path
{:ok, agent} =
  MyApp.CodingAgent.start_link(
    session_store: {Condukt.SessionStore.Disk, path: "/tmp/condukt.session"}
  )
```

When a store is configured, Condukt loads the snapshot at `start_link/1` and
saves a fresh snapshot after every completed turn.

## What a snapshot contains

`Condukt.SessionStore.Snapshot` captures the parts of the session that need
to survive a restart: the message history and the configurable options (such
as model, system prompt, and `cwd`). It does not capture transient state
like in flight tool calls.

## Implementing a store

Implement the `Condukt.SessionStore` behaviour:

```elixir
defmodule MyApp.RedisStore do
  @behaviour Condukt.SessionStore

  @impl true
  def load(opts) do
    case Redix.command(MyApp.Redix, ["GET", key(opts)]) do
      {:ok, nil} -> :not_found
      {:ok, blob} -> {:ok, :erlang.binary_to_term(blob)}
      {:error, reason} -> {:error, reason}
    end
  end

  @impl true
  def save(snapshot, opts) do
    Redix.command(MyApp.Redix, ["SET", key(opts), :erlang.term_to_binary(snapshot)])
    :ok
  end

  @impl true
  def clear(opts) do
    Redix.command(MyApp.Redix, ["DEL", key(opts)])
    :ok
  end

  defp key(opts), do: "condukt:" <> Keyword.fetch!(opts, :key)
end
```

Then plug it into the agent:

```elixir
MyApp.CodingAgent.start_link(
  session_store: {MyApp.RedisStore, key: "user:42"}
)
```

## Clearing history

`Condukt.clear/1` resets the conversation. If a session store is configured,
the next save will overwrite the persisted snapshot with the empty state.