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.

Summary

Functions

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

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

Returns the turns of a session in chronological order.

Lists the stored conversations, newest first.

Starts a session again from the conversation stored under session_id.

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

Starts a session and returns its id.

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

Runs one turn and returns its output.

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

Functions

compact_session(session_id)

@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(session_id)

@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(session_id)

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

Returns the turns of a session in chronological order.

list_sessions()

@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(session_id, options \\ [])

@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(session_id, ledger \\ Mead.Ledger)

@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(options \\ [])

@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(session_id)

@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(session_id, input, options \\ [])

@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(options \\ [])

@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.