Script shape detection, validation, and interpretation for
ALLM.Providers.Fake. See spec §31.
Layer B — pure helper module, no runtime state.
Fake accepts two disjoint script shapes:
- Spec §31 (user-facing) — the vocabulary the spec itself samples with
{:text, "hi"}, {:finish, :stop}. Tags::text,:tool_call,:tool_call_delta,:usage,:raw_chunk,:finish,:error(2-tuple),:delay,:sleep(deprecated alias of:delay). - Phase 3 harness — the vocabulary
ALLM.Test.AdapterConformanceandALLM.Test.StreamAdapterConformancepass toStubAdapter. Tags::ok,:error(3-tuple),:text_delta,:preflight_error,:error_event,:stream_error, and shared-semantics:tool_call/:finish.
This module exposes four entry points:
detect_shape/1— classify a list of entries by leading tag.validate!/1— guardadapter_optsat the Fake boundary.fold_to_response/1— reduce a list of entries into an%ALLM.Response{}(non-streaming path).interpret/1— translate one entry into a list ofALLM.Eventvalues (streaming path).
See spec §31 for the script entry grammar; §8 for the event union.
Summary
Types
A single Phase 3 harness-shape entry (non-streaming).
A single Phase 3 harness-shape entry (streaming).
Detected shape of a script — §31 (user-facing) or Phase 3 harness.
A single spec §31 script entry (one call's worth of events).
Functions
Classify a list of script entries as either :spec31 or :harness shape.
Fold a list of script entries into a single %ALLM.Response{}.
Translate a single script entry into a list of ALLM.Event values.
Validate adapter_opts at the Fake boundary.
Types
A single Phase 3 harness-shape entry (non-streaming).
@type harness_stream_entry() :: {:text_delta, String.t()} | {:finish, atom()} | {:preflight_error, atom(), keyword()} | {:error_event, atom(), keyword()} | {:stream_error, atom(), keyword()}
A single Phase 3 harness-shape entry (streaming).
@type shape() :: :spec31 | :harness
Detected shape of a script — §31 (user-facing) or Phase 3 harness.
@type spec31_entry() :: {:text, String.t()} | {:tool_call, keyword()} | {:tool_call_delta, keyword()} | {:usage, map()} | {:raw_chunk, term()} | {:finish, ALLM.Response.finish_reason()} | {:error, term()} | {:delay, non_neg_integer()} | {:sleep, non_neg_integer()}
A single spec §31 script entry (one call's worth of events).
Functions
Classify a list of script entries as either :spec31 or :harness shape.
Inspects only the leading entry's tag. The :error tag is disambiguated by
tuple arity — 2-tuple → :spec31, 3-tuple → :harness. Shared-semantics
tags (:finish, :tool_call) default to :spec31 (the interpreter handles
both shapes identically for those tags, so the classification is
inconsequential).
An empty list returns {:spec31, []} (the default).
Raises ArgumentError if the leading tag is unknown, with a message that
mentions both vocabularies so script authors can spot typos.
Examples
iex> ALLM.Providers.Fake.Script.detect_shape([{:text, "hi"}, {:finish, :stop}])
{:spec31, [{:text, "hi"}, {:finish, :stop}]}
iex> ALLM.Providers.Fake.Script.detect_shape([{:ok, %{output_text: "hi"}}])
{:harness, [{:ok, %{output_text: "hi"}}]}
iex> ALLM.Providers.Fake.Script.detect_shape([])
{:spec31, []}
@spec fold_to_response([spec31_entry() | harness_adapter_entry()]) :: ALLM.Response.t() | {:error, ALLM.Error.AdapterError.t()}
Fold a list of script entries into a single %ALLM.Response{}.
Handles both the spec §31 vocabulary (:text, :tool_call,
:tool_call_delta, :usage, :raw_chunk, :finish, :error/2,
:delay, :sleep) and the harness-shape terminal entries ({:ok, map},
{:error, reason, opts}). Harness-shape is one entry per call; the
reducer returns on the first harness entry it sees.
A §31 {:error, term} entry short-circuits to
{:error, %AdapterError{reason: :unknown, message: "scripted error", cause: term}}. {:delay, ms} / {:sleep, ms} call Process.sleep/1
so non-streaming callers can still exercise timeout paths. {:raw_chunk, _} is ignored (raw chunks have no place on %Response{}).
{:usage, map} calls struct!(ALLM.Usage, map) with last-write-wins
semantics — passing an unknown field name (e.g., :prompt_tokens) raises
KeyError at fold time; use the canonical field names from
ALLM.Usage (spec §5.9a).
Returns %ALLM.Response{} on success (NOT {:ok, %Response{}} — the
{:ok, _} wrapping is applied at the ALLM.Providers.Fake.generate/2
boundary in sub-phase 4.2), or {:error, %AdapterError{}} on
script-defined failure.
Examples
iex> ALLM.Providers.Fake.Script.fold_to_response([{:text, "hi"}, {:finish, :stop}])
%ALLM.Response{output_text: "hi", finish_reason: :stop, tool_calls: [], usage: %ALLM.Usage{}}
@spec interpret(spec31_entry() | harness_stream_entry()) :: [ALLM.Event.t()]
Translate a single script entry into a list of ALLM.Event values.
Called per-entry by ALLM.Providers.Fake.stream/2. {:delay, ms} and
{:sleep, ms} return [] — the stream runner handles the sleep directly
before emitting the next event, so interpret/1 has no events to yield.
:text_completed is NOT emitted here. Whether to emit it depends on
whether any :text was seen earlier in the same call; that state lives in
the Stream.resource/3 accumulator, not here. interpret({:finish, _})
returns just [{:message_completed, _}]; the stream runner prepends
:text_completed when appropriate. Revisit in sub-phase 4.3 if the
orchestration needs change.
Deprecation
{:sleep, ms} is a deprecated alias for {:delay, ms} (spec §31). Passing
a {:sleep, _} entry triggers a one-time Logger.warning/1 per BEAM
lifetime (dedup via :persistent_term). Deletion target: v0.3.
@spec validate!(keyword()) :: :ok
Validate adapter_opts at the Fake boundary.
Guards (in order):
- Mixing
:scriptand:scriptsraisesArgumentError(spec §31). :scriptmust be a list (of entries).:scriptsmust be a list of lists (each inner list is one call's script).:stream_scriptmust be a list of lists.:script_cursor, when present, must be a pid ornil.
Returns :ok on success.
Users may call this directly on their adapter_opts for construction-time
feedback (Non-obvious Decision #5 in the Phase 4 design doc) — Fake itself
calls it at the first adapter invocation.