ExAgent.Roles (ExAgent v0.3.0)

Copy Markdown View Source

Resolves provider structs from role names declared in application config.

A role names a purpose (:chat, :vision, :embed, :ocr, ...) and maps it to a provider module plus the options its new/1 takes. Application code then names the purpose instead of the vendor, so swapping a hosted model for a self-hosted container is a config change rather than an edit at every call site.

# config/runtime.exs
config :ex_agent, :roles,
  chat: {ExAgent.Providers.Gemini, api_key: System.fetch_env!("GEMINI_API_KEY")},
  vision: {ExAgent.Providers.OpenAICompatible,
             base_url: System.fetch_env!("MODAL_QWEN_URL"),
             model: "Qwen/Qwen3-VL-8B-Instruct",
             modalities: [:text, :image]}

ExAgent.provider!(:vision)   #=> %ExAgent.Providers.OpenAICompatible{}

Role names are arbitrary atoms - the library has no fixed set. A bare module means {module, []}.

Option values resolved at boot

An option value that is a zero-arity function or an {module, function, args} tuple is invoked once during build!/0, for credentials that come from a vault rather than an env var:

chat: {ExAgent.Providers.Gemini, api_key: {MyApp.Vault, :fetch, ["gemini"]}}

Because of this, a literal three-element tuple cannot be used as an option value. No provider option currently takes one.

Caching

Roles are resolved once, during application start, and stored in :persistent_term. Reads are free - no copy, no table lookup - which matters because ExAgent.provider!/1 sits on every request. Writes are not: each one triggers a global GC scan across every process in the VM, which is why build!/0 is the only thing that writes and why per-call overrides (ExAgent.provider!/2) build a fresh struct instead of caching it.

Call build!/0 yourself if you assemble role config after boot.

Security

A cached provider struct holds a Req client with its API key baked into the request headers, so credentials live in :persistent_term and will appear in a VM crash dump. This is the same exposure as holding them in supervisor state, but it is worth knowing.

Summary

Functions

Resolves every configured role into a provider struct and caches it.

Builds a fresh provider struct for role with overrides merged over its configured options.

Returns the cached provider struct for role, or :error if it is not configured.

Returns the cached provider struct for role, raising if it is not configured.

Returns the configured role names, in declaration order.

Functions

build!()

@spec build!() :: :ok

Resolves every configured role into a provider struct and caches it.

Raises ArgumentError, naming the offending role, if a spec is malformed, its module is missing, does not export new/1, does not implement ExAgent.Provider, or its new/1 raises. Failing here means a missing credential crashes at deploy time rather than returning a 401 on the first request.

Every role is built before anything is written, so a bad spec cannot leave a half-applied set behind. Rebuilding replaces the previous set entirely.

build_with!(role, overrides)

@spec build_with!(
  atom(),
  keyword()
) :: struct()

Builds a fresh provider struct for role with overrides merged over its configured options.

The result is not cached - see the caching note in the moduledoc. Building reconstructs the Req client, which is fine per request but not inside a tight loop. Option values already resolved at boot are reused, so a vault-backed credential is not fetched again.

fetch(role)

@spec fetch(atom()) :: {:ok, struct()} | :error

Returns the cached provider struct for role, or :error if it is not configured.

fetch!(role)

@spec fetch!(atom()) :: struct()

Returns the cached provider struct for role, raising if it is not configured.

list()

@spec list() :: [atom()]

Returns the configured role names, in declaration order.