This is the L0 authoritative definition, describing the AgentTurn state machine, atomic I/O ordering, delivery contract, and Inbox/Wakeup constraints.
For the complete L1 implementation contracts, see ../03_kernel_l1/agent_worker.md and ../03_kernel_l1/pmo_orchestration.md.
Topology concepts and patterns are in
01_getting_started/architecture_intro.md; implementation and behavior are governed by this L0 protocol. Historical migration note: runtime dependency onturn_resume_ledgerwas removed during thev1r3 -> v1r4hard cut. Resume now runs only onstate.agent_inbox(deferred/retry_count/next_retry_at). This warning matters for historical upgrades, not for freshV1R4deployments.
- Concurrent execution for the same
agent_idis forbidden (single active Turn). - All state updates must be protected by the
turn_epoch + active_agent_turn_idgating. - Terminal states must contain
task.deliverable, and includedeliverable_card_idinevt.agent.*.task. - Inbox is the execution-side source of truth: execution requests/receipts must be written to
state.agent_inbox; NATS is used only for wakeup. - Single-track resume: retries/replay for
tool_result/timeoutrun only throughstate.agent_inbox(pending/deferred), without runtime dependency onturn_resume_ledger. - One Enqueue equals one Turn: each Enqueue is bound by L0 to an independent
agent_turn_id/turn_epochand performs one delivery (turn_epochcan be created during queued→pending dispatch stage). - Each Turn emits exactly one
evt.agent.*.task. - Wakeup header is only a doorbell: semantic fields follow the inbox records, and the header is not the authoritative audit source.
State enum: idle → dispatched → running ↔ suspended → idle
- idle: no active Turn.
- dispatched: Turn has been claimed/dispatched and is waiting for Worker entry.
- running: Worker is processing (including LLM reasoning and tool calls).
- suspended: asynchronous wait has been started (typically tool callback); waiting set is tracked by
turn_waiting_tools+waiting_tool_count. suspendedno longer defaults to strong binding via a singleexpecting_correlation_id; the current main worker flow clears it during suspend and resumes based on the waiting list.
Observation signals (not state-machine guards):
evt.agent.*.state.metadata.activity:thinking | executing_tool | awaiting_tool_result(best-effort).evt.agent.*.state.metadata.current_tool: current tool name (optional).evt.agent.*.state.metadata.waiting_tool_count: number of tool receipts pending (optional).
State fields:
expecting_correlation_id: reserved field. The current main worker path normally clears this insuspended, while some boundary scenarios (such as UI worker) may write action-related IDs.waiting_tool_count: number of tools still pending (one of the actual resume criteria forsuspended).resume_deadline: timeout-resume time for SUSPENDED (on expiry, a timeout report is injected automatically).
- Write to
state.agent_inbox(pointerized payload + trace/lineage). - Record in
state.execution_edges(primitive="enqueue",edge_phase="request"). - Publish
cmd.agent.{target}.wakeup(control-plane doorbell). - For
message_type="turn", route through L0 queue:- initial inbox write uses
status='queued'with noturn_epochavailable. - when the agent is idle, L0
dispatch_nextwillleaseit todispatchedand promote the queuestatusfromqueuedtopending(while writing backturn_epoch).
- initial inbox write uses
- For
message_type="ui_action"(and some upper-layer direct pushes), performlease_agent_turn_with_connfirst, set head directly todispatched, then write toinbox(typicallypending).
enqueue(primitive="enqueue",edge_phase="request"): records all enqueue entries.report(primitive="report",edge_phase="response"): records report ingestion such astool_result/timeout.join(primitive="join",edge_phase="request|response"): records join-semantic subtask collaboration.
- After receiving wakeup, claim due inbox first (
status in (pending,deferred)andnext_retry_at<=now(),FOR UPDATE SKIP LOCKED, ordered bynext_retry_at, created_at ASC). - State updates use CAS gating (
turn_epoch + active_agent_turn_id) and do not rely on state row locks. - Worker must not create
agent_turn_id/turn_epochitself; missing or illegal inbox is a protocol error and should be rejected. status=idle: L0 generates/takes overagent_turn_idat enqueue/lease time and incrementsturn_epoch, writestrace_id/parent_step_id, and setsstatus=dispatched.status=idle(mailbox): when a queued turn exists, L0 completes lease andqueued→pendingadvancement duringdispatch_next, then wakes processing.status=dispatched: transition torunningwhen entering processing.status=running: enters greedy batch, processing pending inbox items in time order in batches.status=suspended: no longer filters claim queue by a singlecorrelation_id; after wakeup, processing proceeds by actual message routing (includingtool_result/timeout/stop).
Greedy batch does not mean one Turn handles multiple Enqueue entries. Each Enqueue is assigned a new
agent_turn_id/turn_epochby L0. The Worker consumes that Turn, completes delivery, returns toidle, and then processes the next Enqueue.
- Write
tool.callcard. - Call
L0Engine.command_intentto create command signals (transactionally writeexecution_edges(primitive=tool_call,edge_phase=request), then publishcmd.tool.*orcmd.sys.pmo.internal.*after commit). - If
after_execution=suspend:- set
waiting_tool_count, recordresume_deadline. - write/update
turn_waiting_tools(tool_call_idandstep_id) as the resume set. - normally clear
expecting_correlation_id(except compatibility branches). status=running → suspended.
- set
- If
after_execution=terminate: continue execution and return toidleafter writing delivery. - Note: effective after_execution is determined by
tool.result.content.result.__cg_control.after_executionwhen present and valid.
- Report is written to
state.agent_inbox(message_typesemanticized astool_result/timeout/stop/..., carryingcorrelation_id, with primary resolver beingtool_call_id). tool_result/timeoutafter enqueue follows the resume flow:resume_handlervalidatesstate= suspendedand thattool_call_idexists in currentturn_waiting_tools.- receiving corresponding waiting item updates state; if pending items remain, keep
suspended(preserve or refreshresume_deadline), otherwise transition torunningoridlebased onafter_execution. expecting_correlation_idis generally not involved in primary flow matching.- Retryable failures are written back as inbox
deferredwithretry_count/next_retry_at/defer_reason, and are reclaimed by later wakeup/watchdog cycles.
- Write
task.deliverable. - Publish
evt.agent.*.task. - Clear
active_agent_turn_idand return toidle.
- If
status=suspendedandNOW() > resume_deadline:- the Worker Watchdog discovers timeout items via
turn_waiting_tools, constructs a Report withmessage_type=timeout(correlation_id= timed-outtool_call_id), and writes tostate.agent_inbox. - synchronize cleanup/recording of
resume_deadline, wake worker; the resume flow then continues processing. - reconcile claims due
agent_inboxrows byturn_waiting_tools.tool_call_idcorrelation, without passing through a resume-ledger relay.
- the Worker Watchdog discovers timeout items via
For a more complete fallback and timeout mechanism, see
04_protocol_l0/watchdog_safety_net.md.
- Any terminal state (success/failed/stop/watchdog) must best-effort write
task.deliverable. evt.agent.*.taskmust carrydeliverable_card_id.deliverable_card_idmust be one of the cards inoutput_box_id.
task.deliverable.contentmay be text or field list (fields[]).- If upstream provides
task.result_fields, delivery should cover required fields. task.result_fieldsstructure: must be a list of field objects and wrapped asFieldsSchemaContent.evt.agent.*.taskdoes not carry derived semantic content (summary/fields/links, etc.); derived content must be obtained by rereadingdeliverable_card_id.
- Non-normal termination paths (
stop/watchdog) must best-effort append deliverable and returndeliverable_card_id.
- Not UTP-based and does not depend on
resource.tools. - Constraint: only one call to this tool is allowed per turn.
- If
task.result_fieldsexists incontext_box_id, minimal field validation is required.
- context_box_id: read-only input snapshot.
- output_box_id: write-only output container.
- Worker is forbidden from writing cards into
context_box_id; all new cards must be written tooutput_box_id.
Formula: Output_Box = Agent(Context_Box, User_Prompt).
Supplementary notes (visibility boundaries):
context_box_idis an upstream-explicit input snapshot; Worker does not automatically scan/read other boxes in the project.- Cross-Agent/cross-session context sharing requires explicit passing of
box_idand generating a newcontext_box_id(e.g., PMO context packing; may also be done by tools in the future). - Reference:
03_kernel_l1/pmo_context_handling.md output_box_idas rolling memory: each round, Worker incorporates cards fromoutput_box_idinto context (so outputs are read back).- Box replacement: when context compression/rewrite is needed, upper layers can generate new boxes and replace
context_box_id/output_box_idpointers; protocol semantics do not require box_id to remain constant. - Concurrency constraint: concurrent turns must not share the same
output_box_id; concurrent scenarios must duplicate/fork the output box (single-writer guarantee).
- Read
state.agent_state_headand validate gating. - Read
resourceviews (roster/tools/profiles). - Read cards/boxes (
context_box_id/output_box_id). - Enter ReAct loop, following "write card → write state → emit event".
cmd.agent.{target}.wakeup:agent_id(optionalinbox_id/metadata).- Inbox
tool_result:agent_turn_id/agent_id/turn_epoch/tool_call_id/status/after_execution/tool_result_card_id. evt.agent.{agent_id}.task:agent_turn_id/status/output_box_id/deliverable_card_id.evt.agent.{agent_id}.step:agent_turn_id/step_id/phase.
- CAS failure: stop execution immediately, do not produce side effects.
- Worker crash: PMO recovers via timeout and increments
turn_epochto invalidate old instructions. - ToolResult disorder/replay: must be idempotent; duplicate callbacks should only ACK.