Layer-B runtime engine — the composition point for an adapter, declared tools, and default request params/context.
An engine is a plain struct carrying only modules, atoms, and serializable
plain data (no PIDs, refs, funs, or API keys). Call sites merge per-call
overrides via the resolve_* functions: call opts win over engine
defaults.
Building an engine
The deterministic ALLM.Providers.Fake adapter requires no API key:
iex> engine = ALLM.Engine.new(
...> adapter: ALLM.Providers.Fake,
...> adapter_opts: [script: [{:text, "Hi"}, {:finish, :stop}]]
...>)
iex> engine.adapter
ALLM.Providers.FakeSwap ALLM.Providers.Fake for ALLM.Providers.OpenAI,
ALLM.Providers.Anthropic, or ALLM.Providers.Gemini and pass :model
to use a real provider. API keys resolve through ALLM.Keys.
Serializability
Engines are safe to round-trip through :erlang.term_to_binary/1 and JSON
iff every field carries only modules, atoms, or plain serializable data:
:adapter,:tool_executor,:tool_result_encoder,:image_adaptermodule | nil. Modules are restored on JSON decode viaString.to_existing_atom/1; an adapter module not loaded in the BEAM at decode time surfaces as{:_unknown, :atom_decode_failed}via theALLM.Serializer.from_json/1error path ([:adapter] :module_not_loadedin the field-error vocabulary).:adapter_opts— keyword list of serializable values only. A keyword list containing a fun (e.g. a Finch retry callback) is rejected by the JSON encoder (Protocol.UndefinedErrorfrom Jason). Atom values in the kwlist (e.g.adapter_opts: [mode: :strict]) survive ETF but lose type on JSON round-trip (become binaries) — the decoder restores kwlist keys viaString.to_existing_atom/1but passes values through verbatim. This is the same caller-value asymmetry as:params/:context/:metadatabelow; callers whose adapter opts rely on atom values for provider behaviour (e.g.verify: :peer) should convert at the adapter boundary rather than expect round-trip equality through JSON. The same rule applies to kwlist-shaped:retry.:model—String.t | nil.:tools—[ALLM.Tool.t]where each tool's:handlerisnilor{Module, :function}. A tool with an anonymous-function handler is not JSON-serializable (seeALLM.Toolmoduledoc). Each tool's:manualflag (boolean, defaultfalse) controls per-tool opt-out of auto-execution: whenmanual: true,ALLM.chat/3undermode: :autohalts with:manual_tool_callsinstead of running the handler.:params,:context,:metadata— maps of serializable values whose keys are restored as atoms viaString.to_existing_atom/1on JSON decode. Values pass through verbatim — the library does not deep-type caller-supplied data. Non-stdlib struct values (e.g.DateTime,Decimal) survive ETF round-trip but lose type on JSON decode unless the caller supplies a custom decoder; this asymmetry is by design.:retry—:default | false | keyword.:middleware—[]today; reserved for a later version.
Unknown opts keys that are not in the engine-field deny-list are
forwarded to the adapter unchanged via resolve_params/2 — this is
how provider-specific knobs like :reasoning_effort reach the adapter
without the resolver having to know about them.
Summary
Types
Late-resolved model value returned by resolve_model/2. A bare string,
a provider-tagged tuple, a struct (typically %ALLM.ModelRef{} when
the optional LLMDB catalog is loaded — spec §6.3), or nil when the
engine has no model and no override was passed.
Functions
Compose per-call overrides into the engine struct, returning a new engine.
Build an %ALLM.Engine{} from keyword opts.
Set a single entry in the engine's :context map.
Set a single entry in the engine's :params map.
Append a single tool to the engine's :tools list.
Append multiple tools to the engine's :tools list.
Resolve the effective model for an adapter call.
Resolve the effective params map for an adapter call via a shallow merge
of engine.params with opts filtered by the engine-field deny-list.
Resolve the effective tool list for an adapter call using dedup-by-name semantics.
Replace the engine's :model string.
Types
Late-resolved model value returned by resolve_model/2. A bare string,
a provider-tagged tuple, a struct (typically %ALLM.ModelRef{} when
the optional LLMDB catalog is loaded — spec §6.3), or nil when the
engine has no model and no override was passed.
@type retry() :: :default | false | keyword()
@type t() :: %ALLM.Engine{ adapter: module() | nil, adapter_opts: keyword(), context: map(), image_adapter: module() | nil, metadata: map(), middleware: [module()], model: String.t() | nil, params: map(), retry: retry(), tool_executor: module() | nil, tool_result_encoder: module() | nil, tools: [ALLM.Tool.t()] }
Functions
Compose per-call overrides into the engine struct, returning a new engine.
Convenience helper — merge_opts/2 is not a primitive of the resolver
chain; execution functions typically use resolve_model/2,
resolve_tools/2, and resolve_params/2 directly rather than rebuilding
an engine. merge_opts/2 is useful when you want a single engine value
reflecting per-call overrides (e.g. for telemetry or for passing into a
pre-built helper).
Recognized opt keys:
:model— replacesengine.modelviawith_model/2.:tools— dedup-by-name merge viaresolve_tools/2(this does not callput_tools/2, which is naive append).:params— shallow-merge intoengine.params. The value must be a map; a non-map value (e.g., a keyword list) is silently dropped, becauseengine.paramsis itself a map and the merge target is not defined for other shapes.:context— shallow-merge intoengine.context. Same map-only rule as:params.
Any other opts key is silently dropped — unknown keys are for execution functions, not the engine itself.
Total on valid input: given any %ALLM.Engine{} and any keyword list,
merge_opts/2 returns an %ALLM.Engine{} and does not raise.
Examples
iex> a = ALLM.Tool.new(name: "a", description: "a", schema: %{})
iex> b = ALLM.Tool.new(name: "b", description: "b", schema: %{})
iex> engine = ALLM.Engine.new(model: "old") |> ALLM.Engine.put_tool(a)
iex> merged = ALLM.Engine.merge_opts(engine, model: "new", tools: [b], params: %{temperature: 0.9})
iex> merged.model
"new"
iex> Enum.map(merged.tools, & &1.name)
["a", "b"]
iex> merged.params
%{temperature: 0.9}
Build an %ALLM.Engine{} from keyword opts.
Accepts any subset of the documented struct fields; unknown keys raise
KeyError via struct!/2. :adapter may be nil at construction — the
missing-adapter check fires at adapter-call time (surfaces as
%EngineError{reason: :missing_adapter}), not here.
Examples
iex> engine = ALLM.Engine.new(adapter: ALLM.Providers.Fake, model: "fake:m")
iex> engine.adapter
ALLM.Providers.Fake
iex> engine.model
"fake:m"
iex> engine.tools
[]
Set a single entry in the engine's :context map.
:context is opaque to the library; tool handlers receive it as the second
argument when declared with arity 2.
Examples
iex> engine = ALLM.Engine.new() |> ALLM.Engine.put_context(:user_id, 42)
iex> engine.context
%{user_id: 42}
Set a single entry in the engine's :params map.
Keys may be atoms or strings — adapters decide which form they accept at wire time.
Examples
iex> engine = ALLM.Engine.new() |> ALLM.Engine.put_param(:temperature, 0.7)
iex> engine.params
%{temperature: 0.7}
@spec put_tool(t(), ALLM.Tool.t()) :: t()
Append a single tool to the engine's :tools list.
Naive append — does not dedup by :name. Use merge_opts/2 or
resolve_tools/2 when you need opts-win dedup semantics for per-call
overrides.
Examples
iex> tool = ALLM.Tool.new(name: "echo", description: "echo", schema: %{})
iex> engine = ALLM.Engine.new() |> ALLM.Engine.put_tool(tool)
iex> Enum.map(engine.tools, & &1.name)
["echo"]
@spec put_tools(t(), [ALLM.Tool.t()]) :: t()
Append multiple tools to the engine's :tools list.
Naive append (engine.tools ++ more) — does not dedup by :name. Use
merge_opts/2 for per-call override semantics that replace an engine tool
in place when names collide.
Examples
iex> a = ALLM.Tool.new(name: "a", description: "a", schema: %{})
iex> b = ALLM.Tool.new(name: "b", description: "b", schema: %{})
iex> engine = ALLM.Engine.new() |> ALLM.Engine.put_tools([a, b])
iex> Enum.map(engine.tools, & &1.name)
["a", "b"]
@spec resolve_model( t(), keyword() ) :: resolved_model()
Resolve the effective model for an adapter call.
Reads opts[:model] if present, otherwise falls back to engine.model.
When the optional LLMDB model-catalog module is loaded in the BEAM,
delegates to LLMDB.model/1 on the chosen value so the caller can
receive a catalog-backed model ref; when absent (the default), returns
the value verbatim. Core ALLM functions without the catalog.
Examples
iex> engine = ALLM.Engine.new(model: "fake:m")
iex> ALLM.Engine.resolve_model(engine, [])
"fake:m"
iex> ALLM.Engine.resolve_model(engine, model: "override")
"override"
iex> ALLM.Engine.resolve_model(ALLM.Engine.new(model: {:openai, "gpt-x"}), [])
{:openai, "gpt-x"}
Resolve the effective params map for an adapter call via a shallow merge
of engine.params with opts filtered by the engine-field deny-list.
Returned as a map (not a keyword list). Engine-field keys
(:adapter, :adapter_opts, :model, :tools, :tool_executor,
:tool_result_encoder, :image_adapter, :params, :context,
:retry, :middleware, :metadata, :api_key) are never forwarded
they are consumed by the engine layer or by other resolvers. Every
other opts key flows through unchanged, so provider-specific knobs
(:reasoning_effort) and orchestration knobs (:max_turns,
:halt_when) naturally reach the adapter — unknown options in opts
are forwarded to the adapter unchanged.
Examples
iex> engine = ALLM.Engine.new(params: %{temperature: 0.2, top_p: 1.0})
iex> ALLM.Engine.resolve_params(engine, temperature: 0.7)
%{temperature: 0.7, top_p: 1.0}
iex> ALLM.Engine.resolve_params(engine, model: "x", reasoning_effort: "high")
%{temperature: 0.2, top_p: 1.0, reasoning_effort: "high"}
@spec resolve_tools( t(), keyword() ) :: [ALLM.Tool.t()]
Resolve the effective tool list for an adapter call using dedup-by-name semantics.
The result preserves engine.tools order; each engine tool whose :name
matches a tool in opts[:tools] is replaced in place by the matching
opts tool. Opts tools whose :name doesn't match any engine tool are
appended in opts[:tools] order. Example: engine.tools = [a, b, c],
opts[:tools] = [b', d] → [a, b', c, d].
When opts has no :tools key, returns engine.tools unchanged.
Examples
iex> a = ALLM.Tool.new(name: "a", description: "a", schema: %{})
iex> b = ALLM.Tool.new(name: "b", description: "b", schema: %{})
iex> c = ALLM.Tool.new(name: "c", description: "c", schema: %{})
iex> b_prime = ALLM.Tool.new(name: "b", description: "override", schema: %{})
iex> d = ALLM.Tool.new(name: "d", description: "d", schema: %{})
iex> engine = ALLM.Engine.new() |> ALLM.Engine.put_tools([a, b, c])
iex> ALLM.Engine.resolve_tools(engine, tools: [b_prime, d]) |> Enum.map(& &1.name)
["a", "b", "c", "d"]
Replace the engine's :model string.
Examples
iex> engine = ALLM.Engine.new(model: "old") |> ALLM.Engine.with_model("new")
iex> engine.model
"new"