GenAgent.Backends.Codex.EventTranslator (GenAgentCodex v0.5.0)

Copy Markdown View Source

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 for thread_id, no GenAgent event emitted directly. The captured id is threaded into the final :result event as session_id.
  • turn.started -- filtered.
  • item.completed with item.type == "agent_message" -- emits a :text event with the item's text content and a message boundary.
  • item.started for an action emits a small :tool_use marker with its ID and type (plus MCP/collab tool names when present). item.completed emits one :tool_result containing 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-fatal error items emit :tool_result activity records with their item type in :data. They do not become assistant response text or terminal errors.
  • turn.completed -- emits a :usage event (if token counts are present) followed by a terminal :result event carrying the captured thread_id as session_id and the raw completed usage as usage_total. With response_text: :final_message the :result also carries the last completed agent_message text 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 :error event, using the latest notification as a fallback when the failure has no reason.
  • CodexWrapper.StreamError -- emits a terminal :error with 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 :result carries no :text, so Response.text joins every completed agent_message with a blank line.
  • :final_message -- the :result carries the text of the last completed agent_message seen in this invocation as :text. An empty last message gives "", and so does a turn with no agent_message. The :text events 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

response_text()

@type response_text() :: :all_messages | :final_message

What the terminal :result reports as the turn's text.

usage_total()

@type usage_total() :: %{optional(atom()) => non_neg_integer()}

Raw completed usage totals keyed by counter name; absent keys are unknown.

Functions

translate(events, opts \\ [])

Translate a full turn's worth of events.

translate_stream(events, opts \\ [])

@spec translate_stream(Enumerable.t(), keyword()) :: Enumerable.t()

Translate events as they arrive while retaining the thread ID and latest error notification.

Options:

zero_usage_total()

@spec zero_usage_total() :: usage_total()

Baseline for the first turn of a new thread: every counter is zero.