Skip to main content
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

From INIT: the very next scored step transitions to NORMAL. From NORMAL:
  • Enter FAST if the last fast_window (default 6) steps all scored strictly below fast_threshold (0.2).
  • Enter SLOW if the last slow_window (default 5) steps all scored strictly above slow_threshold (0.6).
  • Otherwise stay NORMAL.
From FAST:
  • Return to NORMAL if the current score exceeds fast_threshold + hysteresis_margin (0.3).
  • Otherwise stay FAST.
From SLOW or SKIP:
  • Return to NORMAL if the current score drops below slow_threshold - hysteresis_margin (0.5).
  • Enter SKIP from SLOW if the last skip_window (default 35) steps all scored strictly above skip_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 via fsm_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.
A common pattern is to leave NORMAL on your default model, route FAST to a cheaper Haiku-class model, and route SLOW/SKIP to Sonnet-class. This concentrates compute budget on the steps where it matters.

What the FSM does not do

  • It does not transition to SKIP or any other state based on token_budget. The token budget is tracked-only and exposed via TraceStateManager.get_budget_used().
  • It does not advance to END on a final answer. The transition function never reaches END today.
  • It does not consume monitor signals. Monitor evaluation is independent of FSM state — both run on every scored step.