ALLM.Engine (allm v0.3.1)

Copy Markdown View Source

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

Swap 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_adapter module | nil. Modules are restored on JSON decode via String.to_existing_atom/1; an adapter module not loaded in the BEAM at decode time surfaces as {:_unknown, :atom_decode_failed} via the ALLM.Serializer.from_json/1 error path ([:adapter] :module_not_loaded in 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.UndefinedError from 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 via String.to_existing_atom/1 but passes values through verbatim. This is the same caller-value asymmetry as :params/:context/:metadata below; 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.
  • :modelString.t | nil.

  • :tools[ALLM.Tool.t] where each tool's :handler is nil or {Module, :function}. A tool with an anonymous-function handler is not JSON-serializable (see ALLM.Tool moduledoc). Each tool's :manual flag (boolean, default false) controls per-tool opt-out of auto-execution: when manual: true, ALLM.chat/3 under mode: :auto halts with :manual_tool_calls instead of running the handler.
  • :params, :context, :metadata — maps of serializable values whose keys are restored as atoms via String.to_existing_atom/1 on 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.

t()

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

resolved_model()

@type resolved_model() :: String.t() | tuple() | struct() | nil

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.

retry()

@type retry() :: :default | false | keyword()

t()

@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

merge_opts(engine, opts)

@spec merge_opts(
  t(),
  keyword()
) :: t()

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 — replaces engine.model via with_model/2.
  • :tools — dedup-by-name merge via resolve_tools/2 (this does not call put_tools/2, which is naive append).
  • :params — shallow-merge into engine.params. The value must be a map; a non-map value (e.g., a keyword list) is silently dropped, because engine.params is itself a map and the merge target is not defined for other shapes.
  • :context — shallow-merge into engine.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}

new(opts \\ [])

@spec new(keyword()) :: t()

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
[]

put_context(e, key, value)

@spec put_context(t(), atom() | String.t(), term()) :: t()

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}

put_param(e, key, value)

@spec put_param(t(), atom() | String.t(), term()) :: t()

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}

put_tool(e, tool)

@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"]

put_tools(e, more)

@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"]

resolve_model(engine, opts)

@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_params(engine, opts)

@spec resolve_params(
  t(),
  keyword()
) :: map()

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

resolve_tools(engine, opts)

@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"]

with_model(e, model)

@spec with_model(t(), String.t()) :: t()

Replace the engine's :model string.

Examples

iex> engine = ALLM.Engine.new(model: "old") |> ALLM.Engine.with_model("new")
iex> engine.model
"new"