# `Mead`
[🔗](https://github.com/albertosca/mead/blob/main/lib/mead.ex#L1)

Public API of the `mead` runtime: isolated, supervised conversation sessions.

Each session keeps its history across crashes of its server process. The code that produces
the output of a turn is a `Mead.Session.TurnHandler`, chosen per session.

With `config :mead, :storage, directory: ...` the turns of every session are also written to
disk, and `resume_session/2` brings a stopped session back with the same id and turns. A stored
file is compacted at resume, when a live one grows past `compact_after_bytes`, and on request
with `compact_session/1`.

# `compact_session`

```elixir
@spec compact_session(String.t()) ::
  {:ok, %{bytes_before: non_neg_integer(), bytes_after: non_neg_integer()}}
  | {:error, :not_found | :corrupt_history | {:persistence, term()}}
```

Rewrites the stored conversation as one record per turn and returns the bytes before and after.

Nothing is lost, unless `config :mead, :storage, max_turns:` is set, in which case only that many of the newest turns are kept: `history/1` and `resume_session/2` see the same turns. A running session is
compacted from memory, a stopped one from its file. Returns `{:error, :not_found}` without
storage, for an id that is not a session id, and when no file exists yet,
`{:error, :corrupt_history}` for a damaged file, which is left as it is, and
`{:error, {:persistence, reason}}` when the rewrite failed and the file is untouched.

# `delete_session`

```elixir
@spec delete_session(String.t()) ::
  :ok | {:error, :not_found | {:persistence, term()}}
```

Stops a session if it runs, removes its stored conversation and forgets its ledger aggregate.

Returns `{:error, :not_found}` when there was neither a running session nor a file.

# `history`

```elixir
@spec history(String.t()) :: {:ok, [Mead.Session.Turn.t()]} | {:error, :not_found}
```

Returns the turns of a session in chronological order.

# `list_sessions`

```elixir
@spec list_sessions() ::
  {:ok,
   [%{session_id: String.t(), updated_at: DateTime.t(), running: boolean()}]}
  | {:error, {:persistence, term()}}
```

Lists the stored conversations, newest first.

Each entry has the `:session_id`, the `:updated_at` of its file, and whether the session
`:running` now. Only file times are read, so a damaged conversation is listed too and fails at
`resume_session/2`. Without `config :mead, :storage, directory: ...` nothing is stored and the
list is empty.

# `resume_session`

```elixir
@spec resume_session(String.t(), keyword()) :: {:ok, String.t()} | {:error, term()}
```

Starts a session again from the conversation stored under `session_id`.

It takes the options of `start_session/1`, because the handler and the timeout are not stored.
Turns that were still running when the session ended come back as `:interrupted`. Needs
`config :mead, :storage, directory: ...`; without it nothing is stored and the answer is
`{:error, :not_found}`, which is also the answer for an id with no file or one that is not a
session id. A running session is `{:error, :already_running}`, and a file damaged anywhere but
its last record is `{:error, :corrupt_history}`. The file is compacted first; a failed
compaction only logs a warning.

# `session_usage`

```elixir
@spec session_usage(String.t(), GenServer.server()) ::
  {:ok, map()} | {:error, :not_found}
```

Returns what the ledger counted for one session, or `{:error, :not_found}` for a session that has not completed a turn or was deleted.

# `start_session`

```elixir
@spec start_session(keyword()) :: {:ok, String.t()} | {:error, term()}
```

Starts a session and returns its id.

## Options

  * `:handler` - module implementing `Mead.Session.TurnHandler`; defaults to the
    `:turn_handler` application setting, then to `Mead.Session.EchoHandler`.
  * `:turn_timeout` - milliseconds a single turn may run; defaults to the `:turn_timeout`
    application setting, then to 30 seconds.

Raises `ArgumentError` when `config :mead, :storage` is malformed, rather than quietly running
without the persistence the operator asked for.

# `stop_session`

```elixir
@spec stop_session(String.t()) :: :ok | {:error, :not_found}
```

Stops a session. Its ledger aggregate stays, so a conversation brought back with
`resume_session/2` returns with what it already spent.

The conversation stays on disk when a storage directory is configured; `delete_session/1`
removes it and forgets its ledger aggregate.

# `submit_turn`

```elixir
@spec submit_turn(String.t(), term(), keyword()) :: {:ok, term()} | {:error, term()}
```

Runs one turn and returns its output.

## Options

  * `:route` - `:auto` (the default), `:local`, or `:cloud`; read by `Mead.Router` and ignored
    by other handlers. Any other value returns `{:error, :invalid_route}` before a turn is
    recorded.
  * `:tools` - `:effect` also shows the model tools that have side effects; see
    `Mead.MCP.Policy`. Handed to the handler unchanged and absent when not given.
  * `:interaction` - a `{module, context}` pair, `module` implementing `Mead.Router.Interaction`;
    `Mead.Router` then asks it for permission around every tool call. Anything else returns
    `{:error, :invalid_interaction}` before a turn is recorded.

Returns `{:error, :not_found}` for an unknown session, `{:error, :session_stopped}` when the
session was stopped with `stop_session/1` while the turn was running, and
`{:error, :session_crashed}` when the session's server died for any other reason.

# `usage`

```elixir
@spec usage(keyword()) :: %{totals: map(), routes: [map()], recent: [map()]}
```

Reads the ledger: the totals over every route, the totals per route and provider, and the most recent turns.

## Options

  * `:recent` - how many of the latest turns to include, newest first, from 0 to 100; defaults to 20.
  * `:ledger` - the ledger to read; defaults to `Mead.Ledger`.

It never changes anything. The totals, the routes and the recent turns are three separate reads of the live tables, so a turn counted between them can show in one and not in another; the result is not a single snapshot. When the ledger is not running it exits, as the ledger accessors do.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
