ObanClaude.Agent.Instance (oban_claude v0.4.0)

Copy Markdown View Source

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/2). States:

  • :idle -- ready for a prompt
  • :running -- an Oban job is in flight; further prompts are :postponed and a :state_timeout watchdog guards against a turn that never reports back. A retryable failed attempt (ObanClaude.Agent.job_retrying/2) keeps the machine here and re-arms the watchdog, so :job_timeout should 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"); approve resumes the session with the action (under :approved_args, see below), reject records 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 explicit resume

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).

The claude session id is read off each turn's payload (including rail-stop errors) and threaded into the next turn as resume, so one agent is one persistent claude conversation. 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 with ObanClaude.Args.defaults/1; string keys); default %{}
  • :approved_args -- claude args merged over :args for approve continuations ONLY, so conversational approval actually unlocks something -- e.g. %{"permission_mode" => "accept_edits"} or an allowed_tools grant. Normal turns never carry these. Default %{}.
  • :worker -- the Oban worker module for turns; default ObanClaude.Agent.Job
  • :oban -- the Oban instance name to insert into; default Oban
  • :job_timeout -- the :running watchdog 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)

Summary

Functions

child_spec(arg)

start_link(agent_id, config)