ReqManagedAgents.Provider that runs the agent loop in-process — :request_response
mode, one model call per poll_turn/2 through a pluggable chat_fun.
The chat wire contract is neutral, OpenAI-chat-completions-shaped, plain string-keyed
maps: chat_fun.(%{model:, messages:, tools:}) :: {:ok, response} | {:error, reason}.
The default chat_fun adapts it to ReqLLM.generate_text/3 (optional req_llm dep);
pointing a chat_fun at any OpenAI-compatible endpoint is a bare Req.post — e.g. a
mimir lane (/v1/chat/completions + granted key via model_config) for hard
data-plane budget enforcement.
The chatfun's response must be OpenAI-chat-completions shaped, string-keyed:
`%{"choices" => [%{"message" => message, "finish_reason" => reason} | ], "usage" => usage}wheremessageis%{"role" => "assistant", "content" => text_or_nil}plus an optional"tool_calls" => [%{"id" => id, "type" => "function", "function" => %{"name" => n, "arguments" => json}}], andusageis%{"prompt_tokens" => n, "completion_tokens" => n}` (or absent).
Errors: return {:error, reason}. Transient reasons are retried with exponential
backoff: %{status: 408} / %{status: 500..} (HTTP) and %{reason: atom} /
%{cause: atom} where the atom is one of :timeout | :closed | :econnrefused | :econnreset | :connect_timeout. Wrap transport exceptions down to their atom —
%{reason: %Req.TransportError{...}} will NOT match; return %{reason: err.reason}.
Open opts: :spec (the ReqManagedAgents.Provider.spec/0, also the provision/2
identity handle), :model_config (canonical keys :model, :api_key, :base_url,
:metadata; defaults from spec.model_config), :chat_fun, :max_turns,
:session_id, retry tuning (:max_chat_retries, :retry_backoff_ms, :sleep_fun).
Events are synthesized under the local.* namespace (local.model_response, and the
guard events added by the loop guards) so SessionResult.events stays raw-preserving.
Summary
Types
@type t() :: %ReqManagedAgents.Providers.Local{ chat_fun: (map() -> {:ok, map()} | {:error, term()}), error_counts: %{optional(String.t()) => non_neg_integer()}, history: [map()], max_turns: pos_integer(), model: term(), polls: non_neg_integer(), seen: MapSet.t(), session_id: String.t(), terminal_tool: String.t() | nil, tools: [map()] }