GenAgentCodex

CI Hex.pm Docs

Codex backend for GenAgent, built on top of codex_wrapper.

Provides GenAgent.Backends.Codex, which wraps the codex CLI and translates its NDJSON event output into the normalized GenAgent.Event values the state machine consumes.

Prerequisites

The codex CLI must be installed and on your PATH. See the Codex docs for install instructions.

Installation

def deps do
  [
    {:gen_agent, "~> 0.2.0"},
    {:gen_agent_codex, "~> 0.1.0"}
  ]
end

Quick start

defmodule MyApp.Coder do
  use GenAgent

  defmodule State do
    defstruct [:path, responses: []]
  end

  @impl true
  def init_agent(opts) do
    path = Keyword.fetch!(opts, :cwd)

    backend_opts = [
      cwd: path,
      sandbox: :read_only,
      skip_git_repo_check: true
    ]

    {:ok, backend_opts, %State{path: path}}
  end

  @impl true
  def handle_response(_ref, response, state) do
    {:noreply, %{state | responses: state.responses ++ [response.text]}}
  end
end

{:ok, _pid} = GenAgent.start_agent(MyApp.Coder,
  name: "my-coder",
  backend: GenAgent.Backends.Codex,
  cwd: "/path/to/project"
)

{:ok, response} = GenAgent.ask("my-coder", "What does lib/foo.ex do?")
IO.puts(response.text)

Session continuation

Codex tracks conversation state via a server-side thread_id. The backend captures it from the first thread.started event of a turn and threads it through codex exec resume on subsequent turns -- transparently, no caller code required.

{:ok, r1} = GenAgent.ask("my-coder", "Remember the number 42")
{:ok, r2} = GenAgent.ask("my-coder", "What number did I ask you to remember?")
# r2.text == "42"

Streaming

The backend uses CodexWrapper.Exec.stream/2 and CodexWrapper.ExecResume.stream/2. CodexWrapper 0.5.1 closes CLI stdin, so the earlier Port startup hang is fixed. handle_stream_event/2 receives normalized events as they arrive while ask/3 returns the completed turn. thread.started supplies the ID used by the next turn's exec resume command.

Backend options

Config:

Exec:

These settings are forwarded on fresh and resumed turns. Sandbox and approval policy become supported -c overrides on resume. :working_dir / :cwd remains the subprocess directory on both turns. Options that the resume command cannot preserve (:cd, :add_dirs, :search) fail at session startup with {:error, {:unsupported_resume_option, option}}.

Backend-only:

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.

See GenAgent.Backends.Codex for the full module docs.

Event translation

Codex CLI's NDJSON output is translated into GenAgent.Event values by GenAgent.Backends.Codex.EventTranslator:

Codex event GenAgent event
thread.started captured for thread_id, then filtered
turn.started filtered
item.completed (agent_message) :text
item.completed (tool_call) :tool_use
item.completed (tool_result) :tool_result
item.completed (mcp_tool_call, command_execution, file_change) :tool_use + :tool_result, carrying the complete item including ID, status and output
turn.completed :usage + terminal :result (with captured thread_id as session_id)
turn.failed / error terminal :error
anything else filtered

Unlike Claude, Codex emits thread_id in the first event of a turn, not the terminal one. The streaming translator retains it and injects it into the :result event emitted at the end. item.started and item.updated are ignored; completed items are reported once. Unknown item categories are filtered. A stream that ends without a terminal event returns :no_terminal_event; the wrapper stream API does not report the subprocess exit code.

Testing

# Unit tests only (default, no CLI invocation)
mix test

# Include live integration tests that actually call the codex CLI
mix test --only integration

Integration tests are tagged :integration so they do not run by default. They burn real tokens -- keep them cheap.

License

MIT. See LICENSE.