DifficultyFSM tracks how hard your agent is working over the last several steps. After scoring a step, the FSM looks at the recent difficulty history and transitions between states that summarize the trajectory — coasting, working normally, struggling, or stalled for an extended window. The current state gates E-trace retrieval, sets monitor injection cooldowns, and selects the model when model_routing is configured.
States
END is part of FSMState for forward compatibility but the shipped DifficultyFSM.transition never returns it. Treat it as inert today.Defaults
DifficultyFSM is constructed with these defaults:
Transition rules
FromINIT: the very next scored step transitions to NORMAL.
From NORMAL:
- Enter
FASTif the lastfast_window(default 6) steps all scored strictly belowfast_threshold(0.2). - Enter
SLOWif the lastslow_window(default 5) steps all scored strictly aboveslow_threshold(0.6). - Otherwise stay
NORMAL.
FAST:
- Return to
NORMALif the current score exceedsfast_threshold + hysteresis_margin(0.3). - Otherwise stay
FAST.
SLOW or SKIP:
- Return to
NORMALif the current score drops belowslow_threshold - hysteresis_margin(0.5). - Enter
SKIPfromSLOWif the lastskip_window(default 35) steps all scored strictly aboveskip_threshold(0.85). - Otherwise stay in the current state.
The hysteresis margin prevents the FSM from rapidly flipping when scores hover near a threshold. A single easy step does not exit
SLOW — the score must drop meaningfully below the boundary.What each state does
FAST skips the full E-trace pipeline. Monitor evaluation still runs (loop detection is most useful when the agent is moving fast and might miss that it’s looping). The monitor injection cooldown stretches to every 5 steps so guidance does not pile up on a healthy run.
NORMAL runs the full pipeline: monitors evaluate server-side, E1 is queried if the gate allows it, E2 retrieves up to two patterns, and E3 fires only if it has not already (it fires once on step 0).
SLOW runs the full pipeline and, if model_routing maps "SLOW" to a model, the request is overridden to that model. The cooldown shrinks to every 2 steps so guidance arrives more frequently.
SKIP shares SLOW’s routing and cooldown. It surfaces in the dashboard as a separate state so you can distinguish brief difficulty from extended stalls.
Configuring thresholds
Pass any subset of the FSM parameters viafsm_thresholds. Unspecified values keep their defaults.
Configuring model routing
model_routing is a separate dict keyed by FSM state name. Map any subset of "FAST", "NORMAL", "SLOW", "SKIP" to a model identifier; states without a mapping use whatever model the agent was created with.
What the FSM does not do
- It does not transition to
SKIPor any other state based ontoken_budget. The token budget is tracked-only and exposed viaTraceStateManager.get_budget_used(). - It does not advance to
ENDon a final answer. The transition function never reachesENDtoday. - It does not consume monitor signals. Monitor evaluation is independent of FSM state — both run on every scored step.

