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

StateMeaningE-trace pipelineMonitor cooldown
INITStarting state, before any step has been scored.E3 only (first call).n/a
FASTRecent window is consistently easy.Skipped entirely — no Qdrant, no embeddings.every 5 steps
NORMALDefault operating state.Full E1 / E2 / E3 pipeline.every 3 steps
SLOWRecent window is consistently hard.Full pipeline. Routes to mapped model if any.every 2 steps
SKIPHigh difficulty has persisted for a long window.Same as SLOW.every 2 steps
ENDDefined in the enum but unreachable through the built-in transition function. Reserved for future use.n/an/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:
ParameterDefaultPurpose
fast_threshold0.2A step at or below this score counts as “easy”.
slow_threshold0.6A step at or above this score counts as “hard”.
skip_threshold0.85A step at or above this score counts toward the SKIP gate.
hysteresis_margin0.1Buffer applied when leaving FAST or SLOW to prevent oscillation.
fast_window6Number of consecutive easy steps required to enter FAST from NORMAL.
slow_window5Number of consecutive hard steps required to enter SLOW from NORMAL.
skip_window35Number 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.