0.5.0 (2026-07-25)
Adds Mimir.CloudEvent, a CloudEvents v1.0 envelope, as the ecosystem's uniform
event wrapper. It carries any domain body — a Mimir.Event wire map, a routing
decision record, a metering record — in data, with the CloudEvents context
attributes as siblings. Mimir.Event is unchanged: it has no
id/source/specversion and its ts is monotonic rather than wall-clock, so
CloudEvents is an envelope concern here, a second export edge alongside
Mimir.Event.OTel — not a rewrite of the vocabulary root.
Mimir.CloudEvent— struct plus strictnew/1,from_event/2(wraps aMimir.Event, takingtypefrom the taxonomy anddatafromEvent.to_wire/1),to_wire/1, tolerantfrom_wire/1, andvalid_time?/1.@enforce_keys [:id, :source, :type]. Theid/source/timea CloudEvent needs are supplied by the producer that wraps a body; this module validates their shape and invents none of them.Mimir.CloudEvent.Types— theai.bizinsights.mimir.*typetaxonomy.for_event/1derivesai.bizinsights.mimir.<domain>.<type>from aMimir.Event; one helper per record family. Open, not a closed union: a broker or consumer must never reject an unknown or newertype.memory/1is explicitly provisional — no producer implements that vocabulary yet.datais any JSON value, carried verbatim and never interpreted here; consumers decode pertype. A binary body travels base64-encoded indata_base64instead, and the two are mutually exclusive —new/1rejects being handed both.dataschemaand extension attributes are modeled.from_wire/1preserves every unrecognized top-level string key as an extension andto_wire/1merges them back at the top level, so distributed-tracing context (traceparent/tracestate) and broker-specific attributes survive a parse/render trip intact. An extension may not shadow a modeled attribute.- Construction is strict, the wire is tolerant — the same posture as
Mimir.Event.new/1requires non-emptyid/source/type, validatestime, honors a supplieddatacontenttype, and rejects aspecversionit cannot write.from_wire/1requires those four attributes andspecversion == "1.0", degrades malformed optional hints (time,subject,dataschema) tonil, never drops a body, and never raises. valid_time?/1documents whereDateTime.from_iso8601/1diverges from RFC3339 (-00:00, lowercaset/z, leap seconds are rejected; a space separator is accepted) rather than claiming exactness —new/1hard-rejects on it.- No new runtime dependency; the
~> 1.15Elixir floor is unchanged.
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.