State Machines¶
AgentsPype uses a custom lightweight FSM engine (agentspype.fsm). AgentStateMachine extends StateMachine with agent-specific hooks and a metaclass that injects default states and transitions into every subclass.
Default Structure¶
Every AgentStateMachine subclass receives the following states and transitions automatically (unless the subclass defines them itself):
| Name | Type | Description |
|---|---|---|
starting |
State(initial=True) |
Initial state; the machine begins here |
idle |
State |
Ready state; the machine waits for work here |
end |
State(final=True) |
Terminal state; triggers agent teardown |
start |
Transition | starting → idle |
stop |
Transition | starting → end or idle → end |
The on_enter_end hook on AgentStateMachine calls agent.teardown() automatically when the end state is entered.
Declaring States and Transitions¶
States and transitions are class attributes. You can declare all of them yourself, mix in the defaults, or rely entirely on the defaults.
from agentspype.fsm import State
from agentspype.agent.state_machine import AgentStateMachine
class WorkerStateMachine(AgentStateMachine):
# States — override the defaults entirely
starting = State("Starting", initial=True)
idle = State("Idle")
processing = State("Processing")
paused = State("Paused")
end = State("End", final=True)
# Transitions
start = starting.to(idle)
pick_job = idle.to(processing)
pause = processing.to(paused)
resume = paused.to(processing)
finish = processing.to(idle)
stop = (
starting.to(end)
| idle.to(end)
| processing.to(end)
| paused.to(end)
)
def after_transition(self, event, state):
self.agent.publishing.publish_transition(event, state)
Note
If you define starting, idle, or end yourself, the metaclass will not overwrite them. If you omit any of them, the metaclass fills in sensible defaults.
The StateMachineMeta Metaclass¶
StateMachineMeta injects default starting, idle, end states and start, stop transitions into any StateMachine subclass that does not define them. It also clones inherited states to ensure full isolation between parent and child classes.
This happens at class creation time, before any instance is created. The base class AgentStateMachine itself is excluded from this injection.
# This subclass only defines after_transition — everything else is injected
class MinimalMachine(AgentStateMachine):
def after_transition(self, event, state):
pass
# MinimalMachine has: starting, idle, end, start, stop
print([s.id for s in MinimalMachine.states])
# ['Starting', 'Idle', 'End']
Required Hook: after_transition¶
after_transition is declared @abstractmethod in AgentStateMachine. Every concrete subclass must implement it. It is called after every successful transition.
The most common implementation publishes the transition event:
def after_transition(self, event, state):
if isinstance(self.agent.publishing, StateAgentPublishing):
self.agent.publishing.publish_transition(event, state)
You can also use it to update status, trigger side effects, or log transitions.
Built-in Transition Hooks¶
AgentStateMachine provides several hooks that are already implemented:
before_transition(event, state, source, target)¶
Logs a debug message for every transition where source != target. Override to add pre-transition validation or guards.
on_start()¶
Called when the start transition fires. Automatically calls self.agent.listening.subscribe(). If you override this, call super().on_start() to retain the subscription behavior.
on_stop()¶
Called when the stop transition fires. Sets the internal _should_stop flag to True.
on_enter_end()¶
Called when entering the end state. Calls self.agent.teardown().
Supported Convention Hooks¶
The FSM resolves hook methods by naming convention:
on_enter_<state_id>()— called when entering a stateon_exit_<state_id>()— called when exiting a stateon_<event_name>(**kwargs)— called when an event fires (receives kwargs passed tosend())before_transition(event, state, source, target)— global hook called before every transitionafter_transition(event, state)— global hook called after every transition
The full hook execution order for each transition is:
before_transition(event, state, source, target)— globalbefore_<event>(**kwargs)— per-eventon_<event>(**kwargs)— per-eventon_exit_<state_id>()— if not internal transition- [state change] — if not internal transition
on_enter_<state_id>()— if not internal transitionafter_<event>(**kwargs)— per-eventafter_transition(event, state)— global
Safe Start and Stop¶
Two convenience methods guard against invalid transitions:
# Only calls start() if currently in the initial state
agent.machine.safe_start()
# Only calls stop() if not already in a final state
agent.machine.safe_stop()
Both methods use the f=True flag internally, which forces the transition even if conditions would otherwise block it.
Accessing the Agent¶
AgentStateMachine holds a weakref to its agent to avoid reference cycles. Access it via the agent property:
def after_transition(self, event, state):
self.agent.status.jobs_processed += 1
If the agent has been garbage-collected, accessing self.agent raises RuntimeError("Agent has been deactivated").
BasicAgentStateMachine¶
BasicAgentStateMachine is a ready-to-use concrete implementation included in the library. It relies entirely on the metaclass-injected defaults and implements after_transition to publish transitions when the publishing component is a StateAgentPublishing:
from agentspype.agent.state_machine import BasicAgentStateMachine
class SimpleAgent(Agent):
definition = AgentDefinition(
state_machine_class=BasicAgentStateMachine,
events_publishing_class=StateAgentPublishing,
...
)
Condition: should_stop()¶
AgentStateMachine exposes a should_stop() method that returns True after on_stop() has been called. You can use this as a polling condition in processing loops:
while not agent.machine.should_stop():
agent.machine.pick_job()
process(...)
agent.machine.finish_job()
State Machine Independence¶
Each agent type maintains its own state machine class. Multiple agent types with different state machines coexist without any shared state:
class MachineA(AgentStateMachine):
starting = State("Starting", initial=True)
a_state = State("A")
end = State("End", final=True)
go = starting.to(a_state)
stop = a_state.to(end)
def after_transition(self, event, state): pass
class MachineB(AgentStateMachine):
starting = State("Starting", initial=True)
b_state = State("B")
end = State("End", final=True)
go = starting.to(b_state)
stop = b_state.to(end)
def after_transition(self, event, state): pass
agent_a = AgentA({}) # uses MachineA
agent_b = AgentB({}) # uses MachineB
# Transitions on agent_a don't affect agent_b
agent_a.machine.go()
assert agent_a.machine.current_state == agent_a.machine.a_state
assert agent_b.machine.current_state == agent_b.machine.starting