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
| State | Meaning | E-trace pipeline | Monitor cooldown |
|---|
INIT | Starting state, before any step has been scored. | E3 only (first call). | n/a |
FAST | Recent window is consistently easy. | Skipped entirely — no Qdrant, no embeddings. | every 5 steps |
NORMAL | Default operating state. | Full E1 / E2 / E3 pipeline. | every 3 steps |
SLOW | Recent window is consistently hard. | Full pipeline. Routes to mapped model if any. | every 2 steps |
SKIP | High difficulty has persisted for a long window. | Same as SLOW. | every 2 steps |
END | Defined in the enum but unreachable through the built-in transition function. Reserved for future use. | n/a | n/a |
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:
| Parameter | Default | Purpose |
|---|
fast_threshold | 0.2 | A step at or below this score counts as “easy”. |
slow_threshold | 0.6 | A step at or above this score counts as “hard”. |
skip_threshold | 0.85 | A step at or above this score counts toward the SKIP gate. |
hysteresis_margin | 0.1 | Buffer applied when leaving FAST or SLOW to prevent oscillation. |
fast_window | 6 | Number of consecutive easy steps required to enter FAST from NORMAL. |
slow_window | 5 | Number of consecutive hard steps required to enter SLOW from NORMAL. |
skip_window | 35 | Number of consecutive very-hard steps required to enter SKIP from SLOW. |
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.
from reasonblocks import ReasonBlocks
rb = ReasonBlocks(
api_key="rb_live_...",
fsm_thresholds={
"fast_threshold": 0.15,
"slow_threshold": 0.65,
"skip_threshold": 0.90,
"hysteresis_margin": 0.08,
"fast_window": 8,
"slow_window": 4,
},
)
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.
rb = ReasonBlocks(
api_key="rb_live_...",
model_routing={
"FAST": "anthropic:claude-haiku-4-5-20251001",
"SLOW": "anthropic:claude-sonnet-4-6",
"SKIP": "anthropic:claude-sonnet-4-6",
},
)
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.