Skip to content

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) or False (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:

actionable object · required
The Agent (or any object) whose methods serve as the actions the HSM will call. Pass None only when constructing a temporary HSM for introspection or serialization purposes.
wildcards dict[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.
policy callable | None
A function that selects which transition to attempt from a list of candidate Action objects. If None, 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.
  • True means "done, move on"; False means "not yet, keep me here". An action that needs several ticks (downloading, learning, waiting for a reply) simply returns False until it is finished, then True. 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_time float
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.
timeout float
If the action keeps returning False for longer than this many seconds, the transition is declared complete and the Agent moves to the next state.
delay float
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.

hsm = HybridStateMachine(actionable=my_agent)
hsm.load("behaviors/service_provider.json")

You can also pass a file object directly, which is useful when loading behavior files packaged inside a Python library:

with open("behaviors/listening_to_teacher.json") as f:
    hsm.load(f)

Saving

Serialize an HSM back to a JSON file at any time:

hsm.save("my_behavior.json")

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 back
  • service_requester.json, finds a provider, sends a request, waits for results, then evaluates
  • listening_to_teacher.json, connects to a teacher role, receives streams, and learns from them