State machines
StateMachine runs a fixed set of states and transitions: events move it from state to state, guards decide whether a transition may fire, and actions run on the way. It is in tnsai-intelligence, package com.tnsai.fsm.
A state machine is a standalone object. Agents do not create or drive one; you call it from your own code, for example inside an action.
Example
import com.tnsai.fsm.Event;
import com.tnsai.fsm.Guard;
import com.tnsai.fsm.State;
import com.tnsai.fsm.StateMachine;
import com.tnsai.fsm.Transition;
State idle = State.initial("IDLE");
State processing = State.of("PROCESSING");
State review = State.of("REVIEW");
State approved = State.terminal("APPROVED");
State rejected = State.terminal("REJECTED");
StateMachine sm = StateMachine.builder("ReviewWorkflow")
.states(idle, processing, review, approved, rejected)
.transition(Transition.from(idle).to(processing).on("START"))
.transition(Transition.from(processing).to(review).on("SUBMIT"))
.transition(Transition.from(review).to(approved)
.on("REVIEW_COMPLETE")
.when(Guard.greaterThan("confidence", 0.9)))
.transition(Transition.from(review).to(rejected)
.on("REVIEW_COMPLETE")
.when(Guard.lessThan("confidence", 0.5)))
.build();
sm.start();
sm.fire(Event.of("START"));
sm.fire(Event.of("SUBMIT"));
sm.getContext().set("confidence", 0.95);
sm.fire(Event.of("REVIEW_COMPLETE"));
sm.getCurrentState().getName(); // "APPROVED"
sm.getStatus(); // COMPLETEDWith a confidence of 0.7, neither guard passes: fire returns false and the machine stays in REVIEW.
States
| Factory | Meaning |
|---|---|
State.initial(name) | Where start() begins. A machine needs exactly one. |
State.of(name) | An ordinary state |
State.terminal(name) | Entering it completes the machine |
State.builder(name) adds initial(), terminal(), onEntry(ctx -> ...), onExit(ctx -> ...), timeout(ms) and metadata(key, value). Read metadata with getSpec(key).
Not implemented.
timeout(ms)is stored and shown in diagrams, but nothing enforces it, and theTIMEOUTstatus is never set.
States are equal when their names are equal. Adding a second state with the same name replaces the first.
build() throws StateMachineException when there are no states, no initial state, or a transition that uses a state you did not add. A second initial state throws IllegalStateException. A terminal state is not required.
Transitions
Transition.from(review).to(approved)
.on("REVIEW_COMPLETE")
.when(Guard.greaterThan("confidence", 0.9))
.action((ctx, from, to) -> ctx.set("approvedBy", "auto"))
.priority(10)
.build();on(event)fires on an event with that name.automatic()fires without an event, whenever its guard passes.when(guard)is optional; without one the transition always passes.- When several transitions from the current state match, the highest
prioritywins (default 0). Transition.internal(state).on(event)goes from a state to itself. It still runs the state's exit and entry actions.
StateMachine.Builder.transition(...) takes a Transition or the unbuilt builder.
What a transition does
Firing a transition runs, in order: the source state's exit action, the transition action, and the target state's entry action. Listeners are notified at each step. Entering a terminal state sets the status to COMPLETED.
After start() and after every fire, automatic transitions run until none passes, the machine completes, or a state comes up twice in the same pass.
Each transition counts toward maxTransitions(n), 1000 by default. The transition after the limit sets the status to FAILED and throws StateMachineException.
An exception from a guard or an action propagates out of fire or start, and the status stays RUNNING.
Events
Event review = Event.of("REVIEW_COMPLETE")
.withData("confidence", 0.95)
.withData("reviewer", "agent-1");
double confidence = review.getData("confidence");
String notes = review.getDataOrDefault("notes", "none");
review.hasData("notes"); // falseEvent is a record with a name and an immutable data map. withData returns a new event. During fire, the event is available to guards and actions as ctx.getCurrentEvent().
Context
StateMachineContext is the key-value store that guards and actions share. Pass one with initial data to start(context), or use sm.getContext():
| Method | Meaning |
|---|---|
get, getOrDefault, set, has, remove, clear, getData | The values. set(key, null) throws NullPointerException; use remove. |
getCurrentState(), getCurrentEvent() | Where the machine is and what triggered it |
setResult, getResult | A result value for your own use |
setError, getError, hasError | An error message. The machine sets it when the transition limit is hit. |
getHistory() | StateTransitionRecord(fromState, toState, event, timestamp) for every transition, starting with the entry into the initial state (fromState is null) |
Guards
Guard is a functional interface, evaluate(context):
| Guard | Passes when |
|---|---|
Guard.always(), Guard.never() | Always, never |
Guard.hasKey(key) | The context has the key |
Guard.equals(key, value) | The context value equals value |
Guard.greaterThan(key, n), Guard.lessThan(key, n) | The context value is a number above or below n. A missing value fails. |
Guard.eventDataEquals(key, value) | The current event's data has that value |
Combine guards with and, or and negate, or write a lambda:
Guard safe = Guard.greaterThan("confidence", 0.8).and(Guard.lessThan("risk", 0.3));
Guard notBlocked = Guard.hasKey("blocked").negate();
Guard authenticated = ctx -> ctx.has("userId");Machine status
| Status | Meaning |
|---|---|
NOT_STARTED | Before start(), and after reset() |
RUNNING | Accepting events |
COMPLETED | A terminal state was entered |
FAILED | The transition limit was exceeded |
TIMEOUT | Never set |
fire throws StateMachineException unless the status is RUNNING, so firing after completion is an error. start throws if the machine was already started. reset() clears the context, the current state and the transition count, and sets the status back to NOT_STARTED.
isRunning(), isInTerminalState(), getTransitionCount(), getStates() and getTransitions() describe the machine. getAvailableEvents() lists the event names of the transitions from the current state, without checking their guards.
Listeners
import com.tnsai.fsm.StateMachineListener;
sm.addListener(new StateMachineListener() {
@Override
public void onTransition(StateMachine sm, State from, State to, Event event) {
System.out.println(from.getName() + " -> " + to.getName());
}
});
sm.addListener(StateMachineListener.logging());StateMachineListener has default no-op methods: onStateEntered, onStateExited, onTransition, onNoTransition and onCompleted. The event is null for automatic transitions and for the entry into the initial state. StateMachineListener.logging() logs entries, transitions and completion at INFO through SLF4J.
Diagrams
StateMachineVisualizer turns a machine into text:
| Method | Output |
|---|---|
toMermaid(sm), toMermaid(sm, MermaidOptions) | A Mermaid stateDiagram-v2 |
toDot(sm), toDot(sm, DotOptions) | Graphviz DOT, terminal states as double circles |
toText(sm) | Status, current state, states and transitions |
For the example above, toMermaid(sm) gives:
stateDiagram-v2
%% ReviewWorkflow
[*] --> IDLE
IDLE --> PROCESSING : START
PROCESSING --> REVIEW : SUBMIT
REVIEW --> APPROVED : REVIEW_COMPLETE [guarded]
REVIEW --> REJECTED : REVIEW_COMPLETE [guarded]
APPROVED --> [*]
REJECTED --> [*]
class APPROVED currentStateStates are not listed in the order you added them. Transitions are.
MermaidOptions | Default |
|---|---|
includeTitle | true |
includeStateDescriptions (timeouts) | true |
showGuards | true |
showAutoTransitions | true |
highlightCurrentState | true. It adds a currentState class but no style for it. |
DotOptions | Default |
|---|---|
direction | "LR" |
nodeShape | "ellipse" |
fontName | "Arial" |
showGuards, showAutoTransitions, highlightCurrentState | true |
currentStateColor | "#90EE90" |
Thread safety
StateMachine is not thread-safe. The context and the listener list are concurrent collections, but fire, start and reset are not synchronized. Fire events from one thread, or synchronize around the machine.
Related
- Planning: planners that choose actions from goals instead of fixed transitions.
- Intelligence: the other parts of
tnsai-intelligence.