Agent Workflow State Machine
Magent now models the agent loop as an explicit thread -> turn -> item
ledger. This is the source of truth for agent workflow state. Legacy
session messages and context-items remain as derived projections for
gptel prompt construction, migration, and older tests.
Codex Alignment
Codex exposes a thread event stream at the SDK/app-server boundary:
thread.startedturn.queuedturn.starteditem.starteditem.updateditem.completedturn.completedorturn.failed
Magent keeps the same workflow boundary, adapted to Emacs:
- Provider transport still goes through
gptel-request. - The Magent loop owns tool dispatch, continuation outcomes, abort, and Emacs UI rendering. The turn layer follows Codex-style continuation: tool output drives the next sampling request, while assistant completion ends the turn even when the assistant text is empty.
- No Codex sandbox, seatbelt, bubblewrap, or shell isolation parity is introduced.
The closest Codex references are:
sdk/typescript/src/events.ts: SDK event names and turn/item stream contract.codex-rs/core/src/session/turn.rs: core sampling loop, tool continuation, history recording, and follow-up decisions.codex-rs/app-server/src/bespoke_event_handling.rs: app-server conversion from core events into thread/turn notifications.codex-rs/app-server-protocol/src/protocol/v2/thread.rs: thread status shape.
State Objects
magent-ledger.el defines the canonical ledger:
magent-thread- statuses:
not-loaded,idle,active,system-error,closed
- statuses:
magent-thread-turn- statuses:
queued,in-progress,completed,interrupted,failed,dropped
- statuses:
magent-thread-item- statuses:
pending,in-progress,completed,failed,cancelled
- statuses:
Each user prompt creates one turn. Assistant messages, reasoning blocks, tool invocations, and tool outputs are items under that turn. Reasoning items are never promoted to assistant message text, even when a provider finishes with no visible content.
The transition table is explicit:
| Object | Runtime statuses | Terminal statuses |
|---|---|---|
| thread | not-loaded, idle, active, system-error, closed |
closed |
| turn | queued, in-progress |
completed, interrupted, failed, dropped |
| item | pending, in-progress |
completed, failed, cancelled |
Turn lifecycle:
queued -> in-progress -> completed
-> interrupted
-> failed
-> dropped
Item lifecycle:
pending -> in-progress -> completed
-> failed
-> cancelled
Tool Items
Tool calls and tool results are one item lifecycle, not two persisted records.
The item starts when the model asks for the tool:
(:type tool :status in-progress :name "grep" :input (:pattern "..."))
It completes or fails when the tool result is available:
(:type tool :status completed :output "...")
This differs from the older tool-call plus tool-output split. For
prompt reuse, Magent still projects a completed tool item into gptel’s
historical (tool . PLIST) shape.
If the model asks for a tool and the result arrives later, Magent updates
the same item by call-id. If an older code path records only a tool
result, the loop creates a synthetic turn so the item still has a durable
turn parent.
Persistence
Persistence is snapshot + journal.
journal: append-only in-memory event log. It records lifecycle transitions such asturn-queued,turn-started,item-started,item-completed, andturn-completed. Session files retain a bounded recent tail controlled bymagent-session-journal-max-events. Tool and permission audit retention is owned separately bymagent-audit.el.snapshot: materialized full thread state. It stores the current thread, turns, and items so resume does not need to replay from the beginning every time.
Session JSON now writes both:
{
"snapshot": { "id": "...", "turns": [...] },
"journal": [{ "type": "turn-started", "...": "..." }]
}
Older messages and context-items are kept as projections for gptel
prompt reuse and migration. On load, Magent prefers replaying
snapshot + journal; if
those fields are absent, it migrates legacy messages into a thread
ledger.
Replay semantics:
- Load
snapshotinto materialized thread state. - Attach the retained
journaltail for recovery/history visibility. - Apply only events with
seq > snapshot.last-event-seq.
This means snapshot is the fast restore point and the persisted journal
is a bounded recovery tail. The two are not interchangeable.
Loop Flow
- UI submission enters
magent-runtime-api.el. magent-runtime-api.elcreates a queued ledger turn and records the completed user message item immediately.- When the submission actually starts,
magent-runtime-api.eltransitions the turn toin-progress. magent-agent-processreuses that turn/user item idempotently instead of duplicating the user message.magent-agent-loopconsumes normalized LLM events.- Text and reasoning deltas update materialized in-progress items in the snapshot without appending one journal event per chunk. Terminal item events carry the final content.
- Tool-call events are accumulated until the provider-neutral
tool-call-batch-endevent closes the sampling batch. - Tool dispatch starts
toolitems, records approval metadata when available, and updates those same items tocompletedorfailed. - Tool output returns a continuation outcome such as
tool-output;magent-agent-processowns the decision to rebuild the prompt from session history and start the next sampling request. This keeps tool execution separate from turn continuation policy. - If a post-tool continuation completes with empty assistant text, Magent makes one provider-tools-disabled final-response retry. If that retry only reasons without producing text, one stricter no-reasoning retry is used; an empty strict response fails the turn.
- Assistant completion records an assistant message item and completes the turn.
- Abort, failure, and dropped queued submissions transition the turn to
interrupted,failed, ordropped; in-progress items under an aborted turn are markedcancelled. magent-max-sampling-requestsis an optional compatibility safety guard, disabled by default. Codex does not impose a per-turn sampling cap; when this guard is explicitly enabled, Magent replaces the continuation at the limit with one provider-tools-disabled final request.
Prompt reconstruction is ledger-driven. When a current turn id is known, Magent includes completed history plus that turn only, preventing later queued user submissions from leaking into the active model request.
UI Projection
The supported frontend is agent-shell. magent-agent-shell.el creates an
in-process ACP client implemented by magent-acp.el. ACP session/prompt
requests submit to magent-runtime-api.el and stay pending until the
corresponding runtime turn completes, fails, or is cancelled. The runtime
emits Magent-native observer events; magent-acp.el converts those events to
ACP session/update messages.
magent-runtime-queue.el owns queued/active turn state. The first
implementation uses one global active turn at a time, but submissions are
tagged by runtime session id so cancellation is session-scoped: cancelling one
ACP session removes that session’s queued work and aborts its active turn
without dropping other sessions’ queued turns.
magent-ui.el, magent-ui-legacy.el, and the other legacy UI names covered by
docs/UI_BACKENDS.org are retained unsupported code. They are not part of the
supported UI projection or public command surface.
Codex Differences Still Preserved
Magent intentionally still differs from Codex in these loop-adjacent areas:
- UI is Emacs-native: supported interaction is agent-shell through an in-process ACP client.
- Provider streaming is normalized behind
magent-llm-gptel.el, but transport remains gptel. - Codex core runs a turn as a multi-sampling loop where pending user input, mailbox items, auto-compaction, and tool follow-up can all extend one active turn. Magent keeps request serialization in the runtime queue and uses Codex-style continuation for tool results: tool execution records model-visible output, then the turn layer continues sampling until the model returns assistant completion. Magent does not implement Codex’s app-server mailbox, steering queue, stop hooks, or mid-turn auto-compaction.
- Codex thread status is app-server-facing
notLoaded/idle/systemError/active{activeFlags}. Magent keeps the same top-level status idea but also persists explicit local turn and item statuses because the Emacs session file is the durable source of truth. - Codex rollout history stores response items and turn context items.
Magent stores a ledger
snapshot + journal;messagesandcontext-itemsare projections used for gptel prompt reuse and migration. - Codex provider/model plumbing is native to codex-core. Magent keeps
gptel-requestas the provider transport and normalizes gptel events before the Magent-owned loop consumes them. - Tool execution is serialized by Magent’s tool queue unless individual tool/runtime support is later added. Codex has richer per-tool runtime, approval, MCP, and process execution machinery; that is outside this workflow goal.
- Child agents are durable Magent jobs, documented in
AGENT_JOBS.org, not Codex app-server threads.
Backlog / TODO
No open TODO remains from the agent-workflow/UI refactor backlog. Future hardening candidates:
- Add per-tool renderer plugins for additional structured outputs beyond the generic file/path and grep-style link extraction.
- Add live visual smoke coverage for very large transcripts and long streaming sessions.
Design Review Summary
After this change, the main agent-loop design gap versus Codex is no longer the lack of explicit thread/turn/item lifecycle. Magent now has that lifecycle and persists it.
The remaining differences are deliberate product/runtime boundaries:
- Magent does not have Codex’s app-server lifecycle manager, subscriber model, unloaded thread cache, or active flags beyond the local ledger status.
- Magent does not have Codex’s same intra-turn input queue/mailbox semantics.
- Magent does not use Codex rollout files directly; it keeps its own
session JSON with
snapshot + journal. - Magent does not split tool call and tool output as separate source-of- truth records. A tool is one item whose status and output are updated.