0.4.1 (2026-07-17)
Additive provenance field: Mimir.Event gains path, a materialized call
path — an ordered list of "<kind>:<id>" frames (closed kind union
workflow | workflow_step | agent | conversation) naming the chain of scopes
that contain the event, outermost first, innermost last, defaulting to
[]. One event, in isolation, recreates its full containment lineage;
List.last(path) is the innermost scope the event belongs to (for a leaf
event, its immediate container; for a scope-lifecycle event, the scope
itself). This is deliberately the containment/spawn axis ("what scopes am
I inside"), distinct from any data-dependency axis a caller tracks separately
("whose output did I consume") — the two can diverge and this field only
carries the former. llm/2/agent/2/workflow/2 validate every frame
against the closed kind set (bad kind or empty id → {:error, {:bad_frame, frame}}) — construction only ever writes known kinds. to_wire/1 includes
"path" only when non-empty. from_wire/1 treats path as
malformed-optional data and validates shape only — a well-formed
"kind:id" pair, the kind NOT checked against the closed union — so an
unknown-but-well-formed kind from a newer producer passes through intact
(an additive kind is not a reader-breaking change); a missing key, a non-list,
or a genuinely malformed frame degrades the whole path to [] rather than
failing the parse. Mimir.Event.OTel.render/1 adds a "mimir.path" attribute
(frames joined with /) when path != []; the frozen gen_ai.* byte-compat
goldens are unaffected since none of those fixtures carry a path.
0.4.0 (2026-07-16)
Replaces the gen_ai junk-drawer envelope with a domain-typed event
vocabulary. gen_ai is demoted to what it always should have been: a wire
format at the OTel export edge, not a domain model.
Mimir.Event— the new vocabulary root: a closeddomain(:llm | :agent | :workflow) ×typeunion, typed correlation ids (request_id,workflow_id,step_id,session_id— the correlation spine is unchanged, just promoted to typed fields), promotedusage/toolcommons, and arawcarve-out for anything provider-specific.Event.llm/2,Event.agent/2,Event.workflow/2build it;Event.to_wire/1/Event.from_wire/1are the struct-in-BEAM / JSON-at-the-boundary pair —to_wire/1is the shape downstream storage should persist.Mimir.Event.OTel— the one canonical OTel-attribute mapper for the export edge.llm.*reproduces the retiredMimir.TurnEvents.GenAIhelpers' attribute names byte-for-byte (gen_ai.usage.input_tokens,gen_ai.tool.name,gen_ai.tool.call.id, the baremilestonereasoning marker);agent.*renders OTel GenAI agent semconv (gen_ai.operation.name=invoke_agent,gen_ai.conversation.id);workflow.*is plainmimir.workflow.*— no GenAI pretense.Mimir.TurnEventsis rewritten aroundMimir.Event:append/2takesridand an%Event{}— the buffer, not the caller, ownsseq/ts, overwriting whatever the caller's constructor set.take/1/take_current/0return[%Event{}]in buffer-assigned seq order.Mimir.Ingestpromotes every ingested raw provider map to a%Event{}(domain:llm) before buffering.metadata's"workflow_id"/"step_id"keys are unchanged, now threading into the event's typedworkflow_id/step_idfields instead of a loose payload merge.Mimir.RouteLog.to_meta/2's meta key is renamedgen_ai_events→turn_events(matching the persisted column name the gateway migrates to next); its one entry's payload key is renamed"gen_ai"→"decision". Routing decisions still never enter theMimir.Eventvocabulary —DecisionRecord/RouteLogkeep their own audit shape, by design.
BREAKING
This is a big-bang rename — no deprecation shims, no dual shapes:
Mimir.TurnEvents's oldappend/3(rid, type, gen_ai_map) is replaced byappend/2(rid, %Mimir.Event{}); the oldappend_current/2is replaced byappend_current/1(%Mimir.Event{}).Mimir.TurnEvents.take/1/take_current/0now return[%Mimir.Event{}], not[%{"seq" => _, "ts" => _, "type" => _, "gen_ai" => map()}].Mimir.TurnEvents'senvelope/4is removed.Mimir.TurnEvents.GenAIis removed. Its three builders (reasoning/1,tool_use/1,usage/2) have no drop-in replacement — build aMimir.Eventinstead, and render it at the export edge withMimir.Event.OTel.render/1if you need the old attribute shapes.Mimir.RouteLog.to_meta/2's meta map keygen_ai_eventsis renamedturn_events; its entry's"gen_ai"key is renamed"decision".
Migration: if you persist the old envelope shape
(%{"seq" => _, "ts" => _, "type" => _, "gen_ai" => map()}), adopt
Mimir.Event.to_wire/1 / Mimir.Event.from_wire/1 as the new persisted
form — to_wire/1 is exactly what downstream storage should write instead.
The mimir_gateway 0.4.0-line release is the reference migration for this:
its request_log.gen_ai_events → turn_events backfill transforms every
existing row from the old envelope into Event.to_wire/1's shape in place,
row by row, inside the migration transaction — that transformer is the
worked example to copy for any other store still holding the old shape.
0.3.0 (2026-07-06)
Replaces the routing layer's bare-map vocabulary with typed structs, parsed at a single boundary.
Mimir.RouteResponse— the parsed result of a routing call, withnew/1as the single boundary where a decoded (atom- or string-keyed) wire response becomes mimir's struct vocabulary.Mimir.RouterClient.route/2now returns{:ok, %RouteResponse{}}directly — no ad-hoc atomization downstream.Mimir.Grant,Mimir.Placement,Mimir.Candidate— the leaf structsRouteResponse.new/1parses onto: a minted grant (key, budget, expiry), the flat chosen-model placement (lane, model, runtime), and one catalog entry's routing verdict (chosen, ranked, or excluded).Mimir.Oracle.Placementis renamedMimir.Oracle.Decision— the rich server-side decision (entry, reasons, candidate verdict table), distinct from the wire-levelMimir.Placement.Mimir.DecisionRecordis now a struct (build/5returns a%DecisionRecord{});to_event/1renders it to the binary-keyed audit map. The rendered turn-event shape is unchanged.
BREAKING
Mimir.RouterClient.route/2returns{:ok, %Mimir.RouteResponse{}}instead of{:ok, map()}.Mimir.DecisionRecord.build/5returns a%Mimir.DecisionRecord{}instead of a plain map; itsverdictargument is now{:decision, %Oracle.Decision{}}(was{:placement, %Oracle.Placement{}}).Mimir.Oracle.decide/4returns{:decision, %Oracle.Decision{}}instead of{:placement, %Oracle.Placement{}}.Mimir.Guard.for_grant/3now takes a%Mimir.Grant{}instead of a plain grant map.Mimir.Sessions.opts/2andMimir.Ingest.from_route/2now take a%Mimir.RouteResponse{}instead of a raw route response map.
0.2.0 (2026-07-05)
Adds a governance composition layer on top of the routing oracle:
Mimir.Guard, Mimir.Ingest, Mimir.Sessions.
Mimir.Guard— turn-guard builders for a session loop's between-turn hook.for_grant/3prices the session's accumulated usage against a route response's grant and halts on budget;caps/1is the mimir-less form (turn/token/cost caps, no minted key). Guards never raise mid-run: a pricing-table miss degrades to whatever caps remain and emits a[:mimir, :guard, :pricing_miss]telemetry event (once per process per model).Mimir.Ingest— decision-correlated ingestion of raw session events intoMimir.TurnEvents, keyed by request id with the routing decision's correlation merged into each event's gen_ai map.Mimir.Sessions— the canonical recipe:opts/2turns a route response into amodel_config(granted key plus routedbase_url), aturn_guard, andtelemetry_metadata, ready to splice into a session run.
These three target the documented hook contract of req_managed_agents
0.5.0+ by data shape only — the turn_guard payload shape and the synthetic
"rma.text_delta" event — with no code dependency on that library.
model_config.api_key threading is a harmless opaque passthrough on 0.5.0+
runtimes and activates fully as the enforced grant key once the embedder is
on req_managed_agents 0.6.0.
Also: two new mix mimir.smoke stages (guard, sessions) covering the
composition layer end-to-end.
0.1.0 (2026-07-04)
Initial release.
Modules: Mimir.Descriptor, Mimir.Oracle, Mimir.Catalog, Mimir.Snapshot,
Mimir.Health, Mimir.DecisionRecord, Mimir.RouteLog, Mimir.Pricing,
Mimir.TurnEvents, Mimir.RouterClient (with an HTTP implementation), and
Mimir.Redact.
Design seams as features:
- Injectable model resolver in
Mimir.Catalog— validate or enrich catalog entries through your own registry without touching the oracle. - Explicit-inputs
Mimir.Snapshot— the oracle only ever sees a snapshot the embedder assembled; no hidden reads of process state or global config. - Embedder-owned persistence — decision records and route logs are plain data; whether and how they're stored is entirely the embedder's call.
Also: a mix mimir.smoke task that drives the public API end-to-end as a
repeatable, CI-asserted smoke check, and a mix quality alias (format check,
warnings-as-errors compile, credo, dialyzer) for local and CI use.