Canonical export-edge rendering for Mimir.Event — one mapper, one private
renderer per domain, no per-consumer drift (spec §3.3: "gen_ai is a wire
format at the export edge, not a domain model").
render/1 returns %{type: String.t(), attributes: %{optional(String.t()) => term()}}. type is the domain string ("llm", "agent", "workflow");
attributes is the OTel-attribute-shaped map for that event.
llm domain — byte-compatible with the retired Mimir.TurnEvents.GenAI
The llm mapper reproduces today's exported attribute names exactly for the
three shapes the old Mimir.TurnEvents.GenAI helpers built:
gen_ai.usage.input_tokens/gen_ai.usage.output_tokens,
gen_ai.tool.name/gen_ai.tool.call.id (including the case where the tool
id is nil — the call-id key is still present with a nil value, matching
GenAI.tool_use/1's behavior verbatim), and the bare "milestone" reasoning
marker (note: no gen_ai. prefix on milestone historically — preserved as
documented, not "fixed"). Proof lives in
test/support/fixtures/gen_ai_compat/*.json, captured from the live
Mimir.TurnEvents.GenAI helpers before this module existed (see the fixture
freeze commit and test/mimir/event/otel_test.exs, which assert
render/1's output byte-equal against those frozen fixtures.
llm types with no dedicated historical builder (request_start,
request_stop, turn_complete, exception) had no fixed attribute shape
before this vocabulary existed — the old Mimir.Ingest module's private
classify step forwarded whatever provider-shaped map arrived verbatim as
the event's gen_ai payload. This mapper preserves that posture: it
renders the event's raw map (string-keying any atom keys), so a
collector that stashes provider-native gen_ai.* attributes in raw gets
them exported unchanged.
agent domain — OTel GenAI agent conventions
gen_ai.operation.name is "invoke_agent" for every agent event — verified
against the OpenTelemetry Semantic Conventions for Generative AI Agents
(gen-ai-agent-spans, semconv-genai ~v1.40/1.41, still Development status
as of 2026-07: https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-agent-spans/).
The registry defines two agent operation names, create_agent and
invoke_agent; Mimir's session lifecycle (:session_open/
:session_reattach) is semantically an invocation of an existing or
freshly-brokered agent in both cases, not the OTel create_agent sense
(typically remote agent-service creation, e.g. an Assistants-API
POST /assistants), so both map to invoke_agent.
session_id, when present, renders as gen_ai.conversation.id — the
semconv-registered attribute for correlating a stream of events into one
conversation/session (open-telemetry/semantic-conventions-genai#51).
:turn_start/:turn_end/:terminal/:error have no OTel operation name of
their own (they're sub-moments within one invoke_agent), so the mapper
adds a mimir.agent.event sub-type attribute carrying the Mimir type
string, keeping that distinction legible at the export edge.
:session_open/:session_reattach do not get this extra attribute — the
operation name plus the correlation id already identify the invocation.
workflow domain — plain mimir.workflow.*, no GenAI pretense
Workflow spans do not participate in the GenAI vocabulary at all: render/1
never emits a gen_ai.* key for a :workflow event. Attributes are
mimir.workflow.id, mimir.workflow.step_id (both omitted when nil, same
idiom as Event.to_wire/1's put_present), and mimir.workflow.event
(the Mimir type string, always present).
Summary
Functions
@spec render(Mimir.Event.t()) :: %{ type: String.t(), attributes: %{optional(String.t()) => term()} }