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.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.
Summary
Types
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 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 completed usage_total/0
(default: all zeros).
@spec zero_usage_total() :: usage_total()
Baseline for the first turn of a new thread: every counter is zero.