One agent as one :gen_statem process, its asynchronous turns run as Oban
jobs. Part of the experimental agent layer (see ObanClaude.Agent).
The process never blocks on claude: a prompt enqueues an ObanClaude.Worker
job and the machine parks in :running until the worker's callbacks route
the outcome back as a {:job_finished, payload} cast (via
ObanClaude.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 (ObanClaude.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 claude 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, 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, a correlatedObanClaude.Agent.pause_after_turn/3, or a host-requestedObanClaude.Agent.quiesce/2; 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
ObanClaude.Agent.status/1 reads both without messaging the process. Each
change also emits [:oban_claude, :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 and applied config_revision.
Claude session 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 ObanClaude.Agent.info/1).
Config
start_agent/2 takes a keyword list or map:
:args-- default claude args merged under every turn's prompt (build withObanClaude.Args.defaults/1; string keys); default%{}:approved_args-- claude args merged over:argsfor approve continuations ONLY, so conversational approval actually unlocks something -- e.g.%{"permission_mode" => "accept_edits"}or anallowed_toolsgrant. Normal turns never carry these. Default%{}.:worker-- the Oban worker module for turns; defaultObanClaude.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 opaque non-empty string identifying the immutable host configuration applied to this process; defaultnil:enqueue_fun-- a 2-arity(args, meta) -> {:ok, term} | {:error, term}override of the enqueue itself, for tests (no Oban, no DB)