One agent as one :gen_statem process, its asynchronous turns run as Oban
jobs. Part of the experimental agent layer (see ObanCodex.Agent).
The process never blocks on Codex: a prompt enqueues an ObanCodex.Worker
job and the machine parks in :running until the worker's callbacks route
the outcome back as a {:job_finished, payload} cast (via
ObanCodex.Agent.job_finished/2). States:
:idle-- ready for a prompt:running-- an Oban job is in flight; further prompts are:postponed and a:state_timeoutwatchdog guards against a turn that never reports back. A retryable failed attempt (ObanCodex.Agent.job_retrying/2) keeps the machine here and re-arms the watchdog, so:job_timeoutshould exceed one attempt's backoff plus its execution:waiting_for_user-- the last turn asked a question (structured-output directive"ask_user"); the next prompt is treated as the answer and resumes the Codex session:awaiting_permission-- the last turn requested approval (directive"request_permission");approveresumes the session with the action (under:approved_args, see below),rejectrecords the denial and returns to:idle, and prompts are:postponed until the gate clears. An approved turn that fails or hits the watchdog RE-GATES (same description, fresh id,{:approval_incomplete, reason}in history): the work was approved but not completed, and a re-approval resumes it:paused-- lockdown via:emergency_pause; every call is refused until an explicitresume
Every state change atomically synchronizes the registry value -- the state
atom, paired with the pending action or question in the gated states -- so
ObanCodex.Agent.status/1 reads both without messaging the process. Each
change also emits [:oban_codex, :agent, :transition] telemetry with
%{agent_id, from, to} metadata (state atoms).
The Codex thread id is read off each completed turn and threaded into the next
one as session_id, so one agent is one persistent conversation. Turn count
rides in the data; cost_usd remains 0.0 because Codex doesn't report price.
Config
start_agent/2 takes a keyword list or map:
:args-- default Codex args merged under every turn's prompt (build withObanCodex.Args.defaults/1; string keys); default%{}:approved_args-- Codex args merged over:argsfor approve continuations ONLY, so conversational approval actually unlocks something -- e.g.%{"sandbox" => "workspace_write"}with%{"approval_policy" => "never"}. Normal turns never carry these. Default%{}.:worker-- the Oban worker module for turns; defaultObanCodex.Agent.Job:oban-- the Oban instance name to insert into; defaultOban:job_timeout-- the:runningwatchdog in milliseconds; default 60000:max_history-- cap on retained history entries (newest win); default 500, so an always-on agent's event log cannot grow without bound:enqueue_fun-- a 2-arity(args, meta) -> {:ok, term} | {:error, term}override of the enqueue itself, for tests (no Oban, no DB)