# 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

```java
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();                 // COMPLETED
```

With 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 the `TIMEOUT` status 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

```java
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 `priority` wins (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

```java
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"); // false
```

`Event` 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:

```java
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

```java
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:

```text
stateDiagram-v2
    %% ReviewWorkflow
    [*] --> IDLE
    IDLE --> PROCESSING : START
    PROCESSING --> REVIEW : SUBMIT
    REVIEW --> APPROVED : REVIEW_COMPLETE [guarded]
    REVIEW --> REJECTED : REVIEW_COMPLETE [guarded]
    APPROVED --> [*]
    REJECTED --> [*]
    class APPROVED currentState
```

States 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](planning.md): planners that choose actions from goals instead of fixed transitions.
- [Intelligence](index.md): the other parts of `tnsai-intelligence`.
