Scheduler

This page defines the current contract for Holon's scheduler: what inputs it consumes, how it derives posture and runnability, and what decisions it emits.

Last verified: 2026-05-23 against src/runtime/scheduler.rs, src/runtime/scheduler_executor.rs, src/runtime/waiting.rs, src/runtime/closure.rs.

Source RFCs

Core model

The scheduler is the runtime component that answers: given the current agent state, what should happen next?

It consumes a SchedulerProjection — a snapshot assembled from:

InputSource
Agent statusAgentState.status
Queue depthAgentState.pending
Active tasksTaskRecords with non-terminal status
Current WorkItemcurrent_work_item_idWorkItemRecord
Runnable WorkItemsOpen WorkItems with is_runnable()=true
Wait conditionsActive WaitConditionRecords
Waiting intentsActive WaitingIntentRecords
Wake hintsPendingWakeHint
Turn stateturn_in_progress, last_turn_terminal
Runtime errorsruntime_error_active()

The projection is a read-only snapshot; the scheduler never mutates durable state directly. Decisions are emitted and handed to the executor.

Scheduler inputs (SchedulerInput)

Input variantTrigger
MessageA new message arrived in the agent's queue
IdleSignal::WakeHintA pending wake hint was received
IdleSignal::ContinueActiveA WorkItem was runnable at the last closure
IdleSignal::QueuedAvailableA queued message is ready for processing
IdlePeriodic idle boundary check

Scheduler decisions (SchedulerDecisionKind)

DecisionMeaning
StartModelTurnStart a new model turn with context assembly
ReduceMessageOnlyReduce a message without starting a full model turn
EmitSystemTickEmit a runtime-owned follow-up message (system tick)
WaitForTaskBlock until a non-terminal task completes
WaitForExternalChangeBlock until an external event arrives
WaitForTimerBlock until a timer fires
WaitForOperatorBlock until operator input arrives
SleepRuntime moves the agent to asleep; no immediate action
StayIdleAgent is already asleep; no action
StopAgent is stopped; no scheduling possible
NoopNo action (duplicate suppressed, turn in progress)

Each decision carries metadata: reason, model_reentry, liveness_only, work_item_id, task_id, and evidence.

Decision flow

                    SchedulerInput
                         │
                         ▼
              ┌─────────────────────┐
              │ Status == Stopped?  │──Yes──► Stop
              └─────────┬───────────┘
                        │ No
                        ▼
              ┌─────────────────────┐
              │ Turn in progress?   │──Yes──► Noop
              └─────────┬───────────┘
                        │ No
                        ▼
         ┌──────────────────────────┐
         │ Queue has pending input? │──Yes──► StartModelTurn
         └──────────────┬───────────┘        (or ReduceMessageOnly)
                        │ No
                        ▼
         ┌──────────────────────────┐
         │ Runnable WorkItem?       │──Yes──► EmitSystemTick
         └──────────────┬───────────┘        (ContinueActive)
                        │ No
                        ▼
         ┌──────────────────────────┐
         │ Active wait condition?   │──Yes──► WaitFor{Task,
         └──────────────┬───────────┘         External,Timer,Operator}
                        │ No
                        ▼
                      Sleep

WorkItem scheduling states

WorkItems flow through scheduling states that the scheduler consumes:

StateMeaningScheduler action
RunnableReady for processingMay be auto-picked as current
WaitingOperatorplan_status=NeedsInput or operator waitAgent waits for operator
Blockedblocked_by set without a more specific waitNot runnable; check legacy recheck_at when present
WaitingTaskWait condition on task resultWake on task terminal
WaitingExternalWait condition on external eventWake on external trigger
WaitingTimerRuntime timer waitWake when timer fires
WaitingSystemRuntime system-tick waitEmit system tick
Completedstate=CompletedExcluded from runnable set

Wake/sleep boundary

Known gaps