# `Jido.Storage`
[🔗](https://github.com/agentjido/jido/blob/v2.3.0/lib/jido/storage.ex#L1)

Unified storage behaviour for agent checkpoints and thread journals.

Implementations handle both:
- **Checkpoints**: key-value overwrite semantics for agent state snapshots
- **Journals**: append-only thread entries with sequence ordering

## Built-in Adapters

| Adapter | Durability | Use Case |
|---------|------------|----------|
| `Jido.Storage.ETS` | Ephemeral | Development, testing |
| `Jido.Storage.File` | Disk | Simple durable storage |
| `Jido.Storage.Redis` | Durable | Optional external backing store |

## Implementing Custom Adapters

Implement all 6 callbacks to create a custom storage adapter:

    defmodule MyApp.Storage do
      @behaviour Jido.Storage

      @impl true
      def get_checkpoint(key, opts), do: ...

      @impl true
      def put_checkpoint(key, data, opts), do: ...

      @impl true
      def delete_checkpoint(key, opts), do: ...

      @impl true
      def load_thread(thread_id, opts), do: ...

      @impl true
      def append_thread(thread_id, entries, opts), do: ...

      @impl true
      def delete_thread(thread_id, opts), do: ...
    end

## Concurrency

The `append_thread/3` callback accepts an `:expected_rev` option for
optimistic concurrency control. Implementations should reject appends
when the current revision doesn't match the expected value.

# `append_thread`

```elixir
@callback append_thread(
  thread_id :: String.t(),
  entries :: [Jido.Thread.Entry.t()],
  opts :: keyword()
) ::
  {:ok, Jido.Thread.t()} | {:error, term()}
```

Append entries to a thread.

## Options

- `:expected_rev` - If provided, the append should fail with
  `{:error, :conflict}` if the current thread revision doesn't match.
- `:metadata` - Thread metadata to set (typically only for new threads).

Returns `{:ok, updated_thread}` on success.

# `delete_checkpoint`

```elixir
@callback delete_checkpoint(key :: term(), opts :: keyword()) :: :ok | {:error, term()}
```

Delete a checkpoint by key.

Returns `:ok` even if the key didn't exist.

# `delete_thread`

```elixir
@callback delete_thread(thread_id :: String.t(), opts :: keyword()) ::
  :ok | {:error, term()}
```

Delete a thread and all its entries.

Returns `:ok` even if the thread didn't exist.

# `get_checkpoint`

```elixir
@callback get_checkpoint(key :: term(), opts :: keyword()) ::
  {:ok, term()} | :not_found | {:error, term()}
```

Retrieve a checkpoint by key.

Returns `{:ok, data}` if found, `:not_found` if the key doesn't exist.

# `load_thread`

```elixir
@callback load_thread(thread_id :: String.t(), opts :: keyword()) ::
  {:ok, Jido.Thread.t()} | :not_found | {:error, term()}
```

Load a thread by ID, reconstructing from stored entries.

Returns `{:ok, thread}` if entries exist, `:not_found` if the thread
has no entries.

# `put_checkpoint`

```elixir
@callback put_checkpoint(key :: term(), data :: term(), opts :: keyword()) ::
  :ok | {:error, term()}
```

Store a checkpoint, overwriting any existing value for the key.

# `fetch_checkpoint`

```elixir
@spec fetch_checkpoint(module(), term(), keyword()) ::
  {:ok, term()} | {:error, term()}
```

Fetch a checkpoint and normalize not-found semantics.

Converts adapter-level `:not_found` into `{:error, :not_found}`.

# `fetch_thread`

```elixir
@spec fetch_thread(module(), String.t(), keyword()) ::
  {:ok, Jido.Thread.t()} | {:error, term()}
```

Fetch a thread and normalize not-found semantics.

Converts adapter-level `:not_found` into `{:error, :not_found}`.

# `normalize_storage`

```elixir
@spec normalize_storage(module() | {module(), keyword()}) :: {module(), keyword()}
```

Normalize a storage configuration to `{module, opts}` tuple.

## Examples

    iex> Jido.Storage.normalize_storage(Jido.Storage.ETS)
    {Jido.Storage.ETS, []}

    iex> Jido.Storage.normalize_storage({Jido.Storage.File, path: "priv/jido"})
    {Jido.Storage.File, [path: "priv/jido"]}

