GenAgent.Backends.Codex (GenAgentCodex v0.5.0)

Copy Markdown View Source

GenAgent.Backend implementation backed by CodexWrapper.

CodexWrapper 0.5.6 streams NDJSON while closing CLI stdin. This backend forwards translated events as they arrive, so handle_stream_event/2 can observe progress during a turn.

Session continuation

Codex reports its persistent thread identifier as thread_id in the first thread.started event of a turn, not in the terminal turn.completed event. The EventTranslator captures it and injects it into the :result event as session_id. Under GenAgent, prompt/3 checkpoints the raw ID at thread.started, before a terminal event or interruption. This backend's update_session/2 records a successful result on the session struct, and the next turn is dispatched via ExecResume with that id.

Usage

Codex reports turn.completed.usage as the thread's running total, including on resumed turns. The session keeps the previous completed total, and :usage events carry the increase since then for the five counters Codex reports (input_tokens, output_tokens, cached_input_tokens, cache_write_input_tokens, reasoning_output_tokens). Sessions from start_session/1 start from zero. Sessions from resume_session/2 have no known total, so the first completed turn reports no usage and only records the total. A counter that is missing or decreased (thread reset) produces no delta for that turn, and the new total becomes the baseline. A failed or interrupted turn does not update the baseline, so its consumption is included in the next successful delta.

terminate_session/1 has no native process to close. GenAgent cancels its prompt task on interrupt, watchdog, stop, or agent death, but the default Port runner closes pipes without guaranteeing that the CLI and its subprocesses have exited. Choose a runner with process-group termination when OS-level settlement is required.

Options accepted by start_session/1

Config-level (forwarded to CodexWrapper.Config.new/1):

  • :binary, :working_dir (deprecated alias :cwd), :env, :timeout, :idle_timeout_ms. :timeout bounds the whole CLI turn; :idle_timeout_ms bounds gaps between output frames and defaults to 300,000 ms. Without :timeout, the Forcola runner uses a one-hour whole-run default; GenAgent's watchdog may end the turn earlier.

Exec-level (forwarded to CodexWrapper.Exec):

  • :model, :sandbox, :approval_policy, :full_auto, :dangerously_bypass_approvals_and_sandbox, :skip_git_repo_check, :ignore_user_config, :profile, :config_overrides, :enabled_features, :disabled_features, :images, :output_schema

Options that cannot be preserved on exec resume (:cd, :add_dirs, :search, :ephemeral) are rejected by start_session/1 when enabled. Use :working_dir / :cwd for a directory that persists across turns. The CLI has no global --verbose flag, so verbose: true is also rejected. Explicit false values remain accepted as no-ops. Session options are translated into supported resume arguments; :sandbox and :approval_policy use config overrides because resume does not accept their exec flags. :ignore_user_config applies to both fresh and resumed turns. The CLI accepts :profile only on the initial exec, so it applies to the first turn only. Invalid :sandbox and :working_dir values return {:error, {:invalid_option, key, value}} at session startup.

Backend-only (never forwarded to the CLI):

  • :exec_fn -- a 2-arity function (prompt, session) -> {:ok, Enumerable.t()} | {:error, term()} that replaces the default Exec/ExecResume dispatch. Intended for tests.
  • :response_text -- :all_messages (default) or :final_message. Selects what GenAgent.Response.text holds for a successful turn. See Response text.

Response text

Codex emits one agent_message item per assistant message, so a turn that narrates before answering produces several :text events. By default (response_text: :all_messages) the terminal :result carries no text and GenAgent.Response.text joins every message with a blank line, exactly as before.

With response_text: :final_message, a successful turn.completed puts the text of the last completed agent_message of that turn into the terminal :result as :text, so Response.text holds only the final message. This is useful when the final message is structured output (for example JSON requested via :output_schema) and earlier commentary would break parsing. Exact semantics:

  • Only Response.text changes. Every agent_message still becomes a :text event delivered to handle_stream_event/2 and retained in Response.events. (Core versions that expose Response.final_message derive it from the same terminal text.)
  • The last message wins even when its text is empty: an empty final agent_message yields "".
  • A successful turn with no agent_message yields "".
  • Text is tracked per turn. A resumed turn never reports a message from an earlier turn.
  • Failed turns are unchanged and still return {:error, reason}. Usage deltas and thread checkpointing are unaffected.
  • The option is validated at start_session/1 and resume_session/2. nil means the default. Any other value returns {:error, {:invalid_option, :response_text, value}} before the CLI is called. A resumed session keeps the mode.

Codex has no equivalent of Claude's --system-prompt; if you need system-level instructions, pass them via AGENTS.md in the working directory or through Codex's configuration layer. Unknown keys return {:error, {:unknown_option, key}}; known unsupported system prompt and output cap options return {:error, {:unsupported_option, key}}.

Summary

Types

t()

@type t() :: %GenAgent.Backends.Codex{
  config: CodexWrapper.Config.t(),
  exec_fn: (String.t(), t() -> {:ok, Enumerable.t()} | {:error, term()}),
  exec_opts: keyword(),
  response_text: GenAgent.Backends.Codex.EventTranslator.response_text(),
  thread_id: String.t() | nil,
  usage_total: GenAgent.Backends.Codex.EventTranslator.usage_total()
}