Observable state of one agent (the main loop or a subagent) in an
orchestrated run — the pure functional core of
ExAthena.Orchestrator.Coordinator.
All updates are pure reducers over loop events (apply_event/2) and
todo-list rewrites (set_todos/2), so every state transition is unit
testable without processes (jido-style logic/runtime split).
Status machine
:running ⇄ :waiting_gpu (queue_wait events)
:running ⇄ :stalling (repeated identical conclusions)
:running → :done | :failed ({:done, Result} by category)
any non-terminal → :failed (fail/1 — run process died)Terminal statuses (:done, :failed, :timeout) are sticky.
Todo ledger
The model speaks the dumb full-rewrite todo_write interface (idempotent,
survives small-model slop); this module diffs successive rewrites by
content to assign stable ids — the Claude Code task-ledger pattern,
kept runtime-side so the model never does id bookkeeping. Invariants
enforced in code: at most one :in_progress item (later ones demoted),
and complete_todo/2 for runtime-derived completion (e.g. when a linked
subagent succeeds).
Summary
Functions
Fold one loop event into the agent's observable state.
Flip one todo to completed by id (runtime-derived completion).
Flip the todo matching content to completed (model-facing linkage).
Mark a non-terminal agent failed (the run process died underneath it). Terminal statuses stay as they are.
Create the info record for a newly observed agent.
Apply a full todo-list rewrite from the model, diffing against the current
ledger to keep ids stable (matched by content) and enforcing the
single-in_progress invariant.
Types
@type status() ::
:queued | :running | :waiting_gpu | :stalling | :done | :failed | :timeout
@type t() :: %ExAthena.Orchestrator.AgentInfo{ conclusions: term(), cost_usd: term(), current_action: term(), depth: term(), finished_at: term(), id: term(), iteration: term(), linked_todo: term(), match_text: term(), name: term(), next_todo_id: term(), parent_id: term(), prompt_summary: term(), result: term(), started_at: term(), status: term(), todos: term(), transcript_tail: term(), usage: term() }
@type todo() :: %{ id: pos_integer(), content: String.t(), status: :pending | :in_progress | :completed, active_form: String.t() | nil }
Functions
Fold one loop event into the agent's observable state.
@spec complete_todo(t(), pos_integer()) :: t()
Flip one todo to completed by id (runtime-derived completion).
Flip the todo matching content to completed (model-facing linkage).
Mark a non-terminal agent failed (the run process died underneath it). Terminal statuses stay as they are.
Create the info record for a newly observed agent.
Apply a full todo-list rewrite from the model, diffing against the current
ledger to keep ids stable (matched by content) and enforcing the
single-in_progress invariant.