Flat, pattern-matchable event tuples the loop emits to :on_event.
Event shape inspired by ash_ai's ToolLoop.stream/2 — one tuple per
logical event, exhaustive, easy to match on in LiveView handlers, OTel
emitters, and cost trackers. Same events feed the OpenTelemetry span
emitter (landing in PR 4).
Events:
{:content, text}— partial or full assistant text.{:tool_call, ToolCall.t()}— model requested a tool call.{:tool_result, ToolResult.t()}— tool produced a result (or error).{:tool_ui, %{tool_call_id:, kind:, payload:}}— structured UI payload for hosts that render rich previews (diffs, file contents, process output). Only emitted when a tool returns the{:ok, %{llm:, ui:}}shape; the LLM-facing string still flows through:tool_result.{:iteration, n}— a new iteration is starting.{:compaction, %{before:, after:, reason:}}— context compacted.{:subagent_spawn, %{id:, prompt:}}— a sub-agent started.{:subagent_result, %{id:, text:}}— sub-agent returned.{:usage, usage_map}— partial usage report from the provider.{:structured_retry, %{attempt:, error:}}— extract_structured retry.{:error, reason}— non-fatal warning (the loop continues).{:done, Result.t()}— terminal event. Always the last event emitted; the Result carries the finish_reason.
Summary
Functions
Emit an event via the supplied callback (nil is a no-op).
Types
@type t() :: {:content, String.t()} | {:tool_call, ExAthena.Messages.ToolCall.t()} | {:tool_result, ExAthena.Messages.ToolResult.t()} | {:tool_ui, %{tool_call_id: String.t(), kind: atom(), payload: map()}} | {:iteration, non_neg_integer()} | {:compaction, %{before: integer(), after: integer(), reason: term()}} | {:subagent_spawn, %{id: term(), prompt: String.t()}} | {:subagent_result, %{id: term(), text: String.t()}} | {:usage, map()} | {:structured_retry, %{attempt: non_neg_integer(), error: term()}} | {:error, term()} | {:done, ExAthena.Result.t()}