Translates CodexWrapper.JsonLineEvent values from one Codex turn
into GenAgent.Event values. translate_stream/1 preserves arrival
order and remembers the thread_id reported by thread.started,
adding it to the terminal :result as session_id.
Event mapping
thread.started-- captured forthread_id, no GenAgent event emitted directly. The captured id is threaded into the final:resultevent assession_id.turn.started-- filtered.item.completedwithitem.type == "agent_message"-- emits a:textevent with the item's text content and a message boundary.item.startedfor an action emits a small:tool_usemarker with its ID and type (plus MCP/collab tool names when present).item.completedemits one:tool_resultcontaining the full item. File changes may have only a completion event. Updates to actions are ignored so accumulated output is captured once.- Completed
reasoning,todo_list, and non-fatalerroritems emit:tool_resultactivity records with their item type in:data. They do not become assistant response text or terminal errors. turn.completed-- emits a:usageevent (if token counts are present) followed by a terminal:resultevent carrying the capturedthread_idassession_idand the raw completed usage asusage_total. Withresponse_text: :final_messagethe:resultalso carries the last completedagent_messagetext as:text(see below).error-- remembers the latest notification without ending the turn. If the stream ends before a turn outcome, emits a terminal:error.turn.failed-- emits a terminal:errorevent, using the latest notification as a fallback when the failure has no reason.CodexWrapper.StreamError-- emits a terminal:errorwith the typed{:idle_timeout, ms}or{:timeout, ms}reason.- Unknown event types -- filtered.
If a turn's event list contains no turn.completed, turn.failed, or
error notification, the translator emits nothing terminal. The state machine's
no_terminal_event guard will then deliver {:error, :no_terminal_event}
to the caller.
Usage
Codex fills turn.completed.usage from the thread's running total, so
a resumed turn reports the whole thread so far. The translator takes
the previous completed total as the :usage_baseline option and emits
the per-field increase as :usage. The raw total of the completed turn
is returned in the :result data as usage_total so the backend can
use it as the next baseline.
The five counters are input_tokens, output_tokens,
cached_input_tokens, cache_write_input_tokens and
reasoning_output_tokens. A delta is omitted for a counter when:
- the baseline has no value for it (for example an externally resumed thread whose earlier total is unknown, or a previous turn that did not report it), or
- the counter decreased, which means the thread total was reset and the difference is not this turn's usage.
Omitted deltas are never replaced with zeros or with the raw total.
When no delta remains, no :usage event is emitted. The default
baseline is all zeros, which treats the stream as the first turn of a
thread.
Response text
The :response_text option selects what the terminal :result says
about the turn's text. GenAgent.Response uses a binary :text on
the :result event as Response.text; without it, it joins the turn's
:text events.
:all_messages(default) -- the:resultcarries no:text, soResponse.textjoins every completedagent_messagewith a blank line.:final_message-- the:resultcarries the text of the last completedagent_messageseen in this invocation as:text. An empty last message gives"", and so does a turn with noagent_message. The:textevents are emitted either way, so streaming callers still see every message.
The last message is tracked per translate_stream/2 invocation, which
is one turn. turn.failed and error terminals never carry :text.
Summary
Types
What the terminal :result reports as the turn's text.
Raw completed usage totals keyed by counter name; absent keys are unknown.
Functions
Translate a full turn's worth of events.
Translate events as they arrive while retaining the thread ID and latest error notification.
Baseline for the first turn of a new thread: every counter is zero.
Types
@type response_text() :: :all_messages | :final_message
What the terminal :result reports as the turn's text.
@type usage_total() :: %{optional(atom()) => non_neg_integer()}
Raw completed usage totals keyed by counter name; absent keys are unknown.
Functions
@spec translate( [CodexWrapper.JsonLineEvent.t() | CodexWrapper.StreamError.t()], keyword() ) :: [ GenAgent.Event.t() ]
Translate a full turn's worth of events.
@spec translate_stream(Enumerable.t(), keyword()) :: Enumerable.t()
Translate events as they arrive while retaining the thread ID and latest error notification.
Options:
:usage_baseline-- the previous completedusage_total/0(default: all zeros).:response_text--response_text/0(default::all_messages).
@spec zero_usage_total() :: usage_total()
Baseline for the first turn of a new thread: every counter is zero.