ObanCodex (oban_codex v0.1.0)

Copy Markdown View Source

Run Codex CLI work on an Oban queue.

oban_codex is the thin seam between codex_wrapper and Oban:

The underlying successful payload stays a %CodexWrapper.Result{}. That keeps the seam honest while the helpers offer the ergonomic fields available directly in oban_claude.

Example

defmodule MyApp.CodexJob do
  use ObanCodex.Worker, queue: :codex, max_attempts: 3
end

ObanCodex.Args.new(
  prompt: "summarize the repository",
  working_dir: "/path/to/repo",
  sandbox: :read_only
)
|> MyApp.CodexJob.new()
|> Oban.insert()

Telemetry

Every run emits [:oban_codex, :run, :start], followed by either [:oban_codex, :run, :stop] when the CLI produced a result (including a non-zero exit), or [:oban_codex, :run, :exception] when execution failed before a result was available.

Measurements include :duration in native time units and :cost_usd, which is always 0.0 because Codex's JSON event contract doesn't report price. Metadata includes the stored args and a slim job identity map.

Data handling

Prompts, metadata, JSONL output, and CLI error text can ride on telemetry events. Redact them before exporting telemetry to a log aggregator.

Summary

Types

Maps a wrapper outcome to the {oban_return, payload} envelope.

An Oban.Worker.perform/1 return. :snooze accepts Oban's period form.

The injectable Codex entrypoint: (prompt, query_opts) -> outcome.

The result of the Codex entrypoint.

Functions

Return reported cost in USD.

Parse the Codex JSONL stdout into event structs.

Read the conventional top-level "outcome" string from structured output.

Run one Codex turn from a string-keyed args map.

Return the Codex thread/session id carried by a result or normalized error.

Decode the final agent message as a JSON object or array.

Return the final agent message text, or nil when no message was emitted.

Return token usage from the final turn.completed event, or nil.

Types

classifier()

@type classifier() :: (wrapper_outcome() -> {oban_return(), term()})

Maps a wrapper outcome to the {oban_return, payload} envelope.

oban_return()

@type oban_return() ::
  :ok
  | {:ok, term()}
  | {:error, term()}
  | {:cancel, term()}
  | {:snooze, Oban.Period.t()}

An Oban.Worker.perform/1 return. :snooze accepts Oban's period form.

query_fun()

@type query_fun() :: (String.t(), keyword() -> wrapper_outcome())

The injectable Codex entrypoint: (prompt, query_opts) -> outcome.

wrapper_outcome()

@type wrapper_outcome() ::
  {:ok, CodexWrapper.Result.t()} | {:error, ObanCodex.Error.t() | term()}

The result of the Codex entrypoint.

Functions

cost_usd(arg1)

@spec cost_usd(CodexWrapper.Result.t() | ObanCodex.Error.t() | term()) ::
  number() | nil

Return reported cost in USD.

Codex doesn't currently include price in its JSONL contract, so a normal result returns nil. The error-map clause supports custom query functions.

events(result)

Parse the Codex JSONL stdout into event structs.

outcome(result)

@spec outcome(CodexWrapper.Result.t()) :: String.t() | nil

Read the conventional top-level "outcome" string from structured output.

run(args, opts \\ [])

@spec run(ObanCodex.Args.t(),
  classifier: classifier(),
  query_fun: query_fun(),
  job: Oban.Job.t()
) ::
  {oban_return(), CodexWrapper.Result.t() | ObanCodex.Error.t() | term()}

Run one Codex turn from a string-keyed args map.

The return is {oban_return, payload}: callers get both the Oban verdict and the underlying %CodexWrapper.Result{} or %ObanCodex.Error{}.

Options:

session_id(result)

@spec session_id(CodexWrapper.Result.t() | ObanCodex.Error.t() | term()) ::
  String.t() | nil

Return the Codex thread/session id carried by a result or normalized error.

Current Codex versions emit thread_id; session_id remains a defensive fallback for older versions and forks.

structured(result)

@spec structured(CodexWrapper.Result.t()) :: map() | list() | nil

Decode the final agent message as a JSON object or array.

Codex's --output-schema validates the final message but still emits it as text inside an item.completed event. Scalar JSON values intentionally return nil; structured worker contracts should use an object or array.

text(result)

@spec text(CodexWrapper.Result.t()) :: String.t() | nil

Return the final agent message text, or nil when no message was emitted.

Query runs always force JSONL. For a custom query function that returns plain stdout, a non-empty stdout string is used as a compatibility fallback.

usage(result)

@spec usage(CodexWrapper.Result.t()) :: map() | nil

Return token usage from the final turn.completed event, or nil.