Hybrid state machines¶
The HybridStateMachine (HSM) defines agent behavior in UNaIVERSE, controlling state transitions, multi-step actions, and policy-based selection.
The HybridStateMachine (HSM) is the behavior brain of every UNaIVERSE Agent. You describe behavior as a graph of named states connected by transitions: each transition fires an action (a method on the Agent), and the Agent moves to a new state when that action succeeds. The HSM runs the loop for you, handling multi-step actions, queued requests from other agents, timing, and action selection.
The whole idea in one picture
A behavior is a little map of where the agent can be (states) and what it does to move (actions on the transitions). At any instant the agent sits in exactly one state. Here is the simplest useful behavior, a worker that waits for a job, does it, and goes back to waiting:
stateDiagram-v2
direction LR
[*] --> idle
idle --> working: a request arrives, run process
working --> idle: send the result back
Read it out loud: "While idle, when a request arrives I run process and
move to working; while working, I send the result back and return to idle."
That sentence is the behavior. Everything else on this page is just how you
write that sentence down, in code or in a JSON file.
Key Concepts¶
-
States
Named conditions the Agent can be in, such as
"idle","teaching", or"waiting_for_partner". One state is always active at a time. -
Actions
Async methods on the Agent that are called during transitions or as in-state behaviors. Every action must return
True(success) orFalse(retry next tick). -
Transitions
Rules that define which action to call and which state to move to if that action succeeds. A state can have multiple outgoing transitions.
-
Policy
A function that selects which available transition to attempt next. The default policy tries queued requests first, then the first ready transition.
Constructing an HSM¶
HybridStateMachine takes three constructor arguments:
actionableobject· required- The Agent (or any object) whose methods serve as the actions the HSM will call. Pass
Noneonly when constructing a temporary HSM for introspection or serialization purposes. wildcardsdict[str, str | float | int] | None- A dictionary of named template variables and their initial values. Wildcards embedded in action argument strings (as
{name}) are substituted at runtime. See the Wildcards section below. policycallable | None- A function that selects which transition to attempt from a list of candidate
Actionobjects. IfNone, the built-in policy is used: prioritize pending requests, then choose the first ready transition.
Defining an HSM in Code¶
Build an HSM programmatically by adding states and transitions:
from unaiverse.hsm.hsm import HybridStateMachine
hsm = HybridStateMachine(actionable=my_agent)
# Add states
hsm.add_state("idle")
hsm.add_state("working")
# Add transitions: from_state -> action -> to_state
hsm.add_transit("idle", "working", action="process", args={"num_steps": 5})
hsm.add_transit("working", "idle", action="show", args={})
When the Agent is in "idle", the HSM runs process for five steps. If it
returns True, the Agent moves to "working". On the next successful tick,
show fires and the Agent returns to "idle".
That code draws exactly this graph:
stateDiagram-v2
direction LR
[*] --> idle
idle --> working: process (num_steps 5)
working --> idle: show
Each box is a state you named with add_state. Each arrow is one add_transit
call: it carries the action to run and the state to land in if that
action succeeds. Reading a behavior is always this mechanical, follow the arrows.
Some args keys shape the request, not the method
A handful of names in a transition's args are not passed to your action's
parameters, they configure the interaction the
transition creates: streams, num_steps, target, timeout,
data_samples, callback, volatile, and copy_sys. That is why
args={"num_steps": 5} works even though process declares no num_steps
parameter, it tells the HSM to run the action for five steps. Every other key
in args is matched against your action's real parameters, and an unknown name
is rejected at build time.
How a tick works¶
You never write the loop yourself, the HSM runs it for you. Understanding one tick is the key to understanding every behavior, so here it is in full.
On each tick the agent first runs the in-state action of its current state
(the optional action= you can attach to a state), then asks its policy to
pick one outgoing transition to attempt:
flowchart TD
A([New tick]) --> B[Run the current state's in-state action]
B --> C{Policy picks one transition}
C -->|1. a queued request from a peer| D[Fire that action]
C -->|2. else the first ready transition| D
C -->|3. nothing feasible right now| W[Wait for the next tick]
D --> F{Did the action return True?}
F -->|Yes| G[Move to the target state]
F -->|No| H[Stay in place, try again next tick]
G --> A
H --> A
W --> A
Two things about that picture matter for world building:
- Requests from other agents come first. When a peer asks this agent to do
something (via
send), the request lands in a queue on the matching transition. The default policy always drains those queued requests before the agent's own self-driven transitions. This is why an agent can "wait quietly" in a state with only request-driven transitions, it does nothing until someone asks. Truemeans "done, move on";Falsemeans "not yet, keep me here". An action that needs several ticks (downloading, learning, waiting for a reply) simply returnsFalseuntil it is finished, thenTrue. The HSM keeps the agent in the same state in the meantime. No threads, no callbacks to manage.
Ready transitions and the ready flag
A transition is only a candidate when it is ready. Self-driven
transitions are ready by default; request-driven ones become ready when a
matching request is queued. Marking a transition ready=False (the common
pattern for a "service" state) means "never fire this on my own, only when a
peer asks for it." This single flag is what separates an agent that acts on
its own from one that purely responds to others.
Timing Controls¶
Transitions accept optional timing parameters to control pacing:
total_timefloat- Maximum total wall-clock seconds the transition (a multi-step action) is allowed to run before it is considered complete regardless of the action's return value.
timeoutfloat- If the action keeps returning
Falsefor longer than this many seconds, the transition is declared complete and the Agent moves to the next state. delayfloat- Minimum number of seconds to wait before attempting this transition for the first time.
hsm.add_transit(
"waiting", "processing",
action="receive_data",
args={},
timeout=30.0, # Give up waiting after 30 seconds
delay=1.0 # Wait at least 1 second before trying
)
Loading from JSON¶
HSMs can be defined entirely in JSON files and loaded at runtime. This is exactly how Worlds configure their Agents: each role maps to a .json behavior file in the world_folder, and the World loads and delivers the right file to each connecting Agent.
You can also pass a file object directly, which is useful when loading behavior files packaged inside a Python library:
Saving¶
Serialize an HSM back to a JSON file at any time:
The saved file captures all states, transitions, timing parameters, wildcards, and the welcome message, everything needed to reconstruct the exact same behavior later.
Wildcards¶
Wildcards are named template variables embedded in action arguments that get filled in at runtime. This lets you write a single generic behavior file and customize it for each Agent session without editing the JSON.
# Set multiple wildcards at once
hsm.set_wildcards({"target": "AgentBob", "num_steps": 10})
# Update a single wildcard
hsm.update_wildcard("target", "AgentAlice")
In the JSON file, wildcards appear as {target} or {num_steps} inside action argument strings. When the HSM applies wildcards, every occurrence is replaced with the current value.
json
{
"transitions": {
"idle": [
{
"on": { "action": "send", "args": { "target": "{partner}", "action_name": "process" } },
"goto": "waiting"
}
]
}
}
python
hsm.load("behavior_template.json")
hsm.update_wildcard("partner", "AgentBob")
Visualizing¶
The HSM can render itself as a Graphviz directed graph, useful for documenting World designs or debugging complex behavior flows:
# Get a graphviz.Digraph object for custom rendering
graph = hsm.to_graphviz()
# Save directly to a PDF file
hsm.save_pdf("my_agent_behavior.pdf")
States are shown as nodes and transitions as labeled edges. Multi-step and teleport transitions are visually distinguished from single-step ones.
Tip
The UNaIVERSE examples repository includes several ready-made behavior JSON files you can use as starting points or drop straight into your own World:
service_provider.json, waits for requests, processes them, and sends results backservice_requester.json, finds a provider, sends a request, waits for results, then evaluateslistening_to_teacher.json, connects to a teacher role, receives streams, and learns from them