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/3). 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/3) 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, plus any per-approval:args, see below),rejectrecords the denial and returns to:idle(or:pausedwith a safe-boundary latch), 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, or a correlatedpause_after_turnorquiescelatch applied at a safe terminal boundary; 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). Turn transitions also carry
the wrapper-owned generation, turn and arc identities, plus the optional
application correlation_id. Lifecycle telemetry carries the host's
optional config_revision. Deferred-pause transitions add :cause,
:pause_reason, and :pause_action; gate decisions also add :gate_outcome
and, for permission gates, :action_id.
Codex thread ids are retained in bounded, host-named conversation arcs.
Prompts that omit an arc use "default", preserving the original one-agent,
one-conversation behavior. Turn count and accumulated cost ride in the data
(see ObanCodex.Agent.info/1).
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:session_arcs-- optional%{arc_id => session_id}seed map for restoring provider conversations after this process restarts; default%{}:max_session_arcs-- maximum retained provider handles; least-recently used inactive arcs are evicted as new ones arrive; default 32:config_revision-- optional non-empty opaque string supplied by the host. It is exposed in Agent info, job metadata, and lifecycle telemetry so the host can attribute work to the effective configuration that produced it; defaultnil:enqueue_fun-- a 2-arity(args, meta) -> {:ok, term} | {:error, term}override of the enqueue itself, for tests (no Oban, no DB)