name: flying-state-machines description: > Use this skill when the user needs to implement deterministic or probabilistic finite state machines (FSMs/Markov chains), state transitions, or game AI logic. Apply even for workflow automation, NPC behavior, or decision systems where state-based logic would help, even if they don't explicitly mention "FSM" or "state machine." version: 0.3.1
Flying State Machines Agent Skill
Helps AI agents use the flying-state-machines library for deterministic and probabilistic finite state machines (FSMs/Markov chains).
Quick Start
Create an FSM by subclassing FSM and defining rules and initial_state:
from enum import Enum, auto
from flying_state_machines import FSM, Transition
class State(Enum):
WAITING = auto()
GOING = auto()
class Event(Enum):
START = auto()
STOP = auto()
class Machine(FSM):
initial_state = State.WAITING
rules = set([
Transition(State.WAITING, Event.START, State.GOING),
Transition(State.GOING, Event.STOP, State.WAITING),
])
Process events:
machine = Machine()
machine.input(Event.START) # Returns new state
machine.current # State.GOING
machine.previous # State.WAITING
Core Concepts
Accessing State: An instance of an FSM subclass will have current, next, and previous properties.
fsm.currentwill always have the current state, starting with theinitial_state.fsm.previoushas the state prior to the most recent transition, if any.fsm.nextis populated with the target state before event hooks fire. It remains populated if the transition is canceled, or is cleared after a successful transition.
States and Events: Use Enum or str. Enums are preferred for type safety.
Transitions: Define state changes with Transition(from_state, event, to_state, probability=1.0).
Context Dict: Each FSM instance has context dict for storing arbitrary state data. Passed to hooks and dynamic probability callbacks.
Hooks:
- Event hooks: Fire before state changes. Return
Falseto cancel. - Transition hooks: Fire after state changes occur.
Async Usage
AsyncFSM and AsyncTransition provide async versions of the sync classes.
Only input() and trigger() are async def and must be awaited; all other
methods are synchronous.
import asyncio
from flying_state_machines import AsyncFSM, AsyncTransition
class Machine(AsyncFSM):
initial_state = 'waiting'
rules = set([
AsyncTransition('waiting', 'start', 'going'),
])
async def main():
machine = Machine()
state = await machine.input('start')
print(state) # 'going'
asyncio.run(main())
Hooks attached to AsyncTransition can be sync or async — the library
handles both transparently (detects coroutines and awaits them).
Common Patterns
Deterministic Transitions
The example in the Quick Start section above shows a deterministic FSM.
Probabilistic Transitions
class Roulette(FSM):
initial_state = 'safe'
rules = set([
Transition('safe', 'spin', 'safe', 5/6),
Transition('safe', 'spin', 'dead', 1/6),
])
Dynamic Probabilities (Context-Aware)
def attack_probability(context: dict) -> float:
return 1.0 if context.get('strength', 0) > 0.5 else 0.0
class Guard(FSM):
initial_state = 'patrol'
rules = set([
Transition('patrol', 'see_enemy', 'attack', attack_probability),
Transition('patrol', 'see_enemy', 'retreat', lambda ctx: 1.0 - attack_probability(ctx)),
])
guard = Guard(context={'strength': 0.8})
guard.input('see_enemy') # Will attack
Event Hooks (Can Cancel)
def validate_condition(event, fsm, data) -> bool:
return fsm.context.get('some_val', 1.0) > 0.5
# Can be added and removed
machine.add_event_hook(Event.START, validate_condition)
machine.remove_event_hook(Event.START, validate_condition)
Transition Hooks (Cannot Cancel)
transition = machine.would(Event.START)[0]
def log_transition(transition, context, data):
print(f"{transition.from_state} -> {transition.to_state}")
# Can be added and removed
machine.add_transition_hook(transition, log_transition)
machine.remove_transition_hook(transition, log_transition)
Batch Transitions with from_any/to_any
class NPC(FSM):
initial_state = State.IDLE
rules = set([
# From any state to specific state (e.g. abort to error)
*Transition.from_any(State, Event.ERROR, State.ERROR, 1.0),
# From specific state to any state (e.g. Markov chain)
*Transition.to_any(State.IDLE, Event.WAKEUP, [State.ACTIVE, State.ALERT], 0.5),
])
Custom Random Function
def deterministic_random():
return 0.3 # Always returns same value
machine = Machine(random=deterministic_random)
See the readme.md file for a concise implementation of a PRNG using sha256.
Serialization
Important: Hooks and callables cannot be serialized. Must be re-supplied on unpack.
# Pack
packed = machine.pack()
# Unpack - must inject dependencies
unpacked = Machine.unpack(
packed,
inject={'State': State, 'Event': Event}, # likely required
transition_hooks={transition: [hook]}, # optional
event_hooks={Event.START: [my_hook]}, # optional
random=my_random_func # optional
)
Debug Visualization (Optional)
fsm.touched() # Returns ASCII art showing FSM state and transitions
Best Practices
- Use Enum states and events for better error messages and type checking.
- Use
fsm.contextdict for instance-specific data (e.g., NPC stats, game state) instead of global variables. - Total probability for transitions from a state should be <= 1.0; library normalizes if needed.
- Query before transition: Use
can(event)orwould(event)to check if an event is valid. - Test probabilistic logic with custom random function for deterministic testing.
- Event hooks return bool: Return
Trueto proceed,Falseto cancel. - Transition hooks don't return: They only observe, cannot cancel; good for side effects.
Common Gotchas
- Don't use
FSMdirectly; always subclass withrulesandinitial_stateset. - Rules must be a set:
Transitionobjects are hashable for set membership. - Event hooks fire first: If any returns
False, transition is canceled (transition hooks won't fire). - Context updates: Modifying
contextin hooks affects subsequent event processing. - Serialization: Enums and callables require
injectdict on unpack. Hooks must be re-added. - Multiple transitions: If multiple valid transitions exist with same event/state, one is chosen probabilistically.