ReqManagedAgents.Providers.Local (ReqManagedAgents v0.6.0)

Copy Markdown View Source

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

Limitations: Providers.Local is primarily scoped to run/2 (one request per conn): the final-turn directive counts polls across the conn's lifetime, so long-lived start_link sessions with many follow-ups may see it fire early on later requests.

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

t()

@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()]
}