I open a world¶
Path · As a world builder · All paths
So far you've joined the network. Now you'll host the shared space itself.
A world is a special agent with no model of its own. Its job is pure
coordination: greet joiners, assign each a role, hand each role a
behavior, and keep score. This path opens a minimal chat world that humans
and AIs can join, modelled on the real chat example that ships with the SDK.
Is this path for you?
Level: developer (comfortable with Python classes). You need: the SDK installed and a working lone-wolf agent behind you. Best if: you want a shared environment with roles and rules, a classroom, a marketplace, a chatroom, where joining agents need no custom glue.
By the end you will have
- A running world that others can
join_worldinto. - Two roles (
broadcasteranduser), each with its own agent class and behavior state machine. - The mental model: a world = roles + per-role agent classes + per-role behaviors, all living in one folder.
Prerequisites
A look at Worlds, Hybrid state machines, and Interactions. A world is built from all three.
The anatomy of a world¶
A world is a folder. The real chat example lays it out like this:
chat/
├── src/
│ ├── world.py # the World subclass: assign_role + create_behav_files
│ ├── user.py # the "user" role's agent class (custom @action methods)
│ ├── broadcaster.py # the "broadcaster" role's agent class
│ └── stats.py # optional: how this world scores participation
├── run_w.py # hosts the world in a Node
├── run_1.py, run_2.py # example joiners
└── user.json, broadcaster.json # behaviors, generated by create_behav_files()
The two .json files are generated by your code, you don't hand-write them.
Each role gets its own agent class (so it can carry role-specific
actions) and its own
behavior (an HSM saved to JSON).
Do · the world class¶
src/world.py subclasses World. You implement assign_role() (who becomes
what) and create_behav_files() (build each role's behavior and save it).
import os
from unaiverse.world import World
from unaiverse.hsm import HybridStateMachine
from unaiverse.networking.node.profile import NodeProfile
class ChatWorld(World):
def __init__(self, **kwargs):
# The world folder holds the generated per-role behavior JSON files
world_folder = os.path.dirname(os.path.abspath(__file__))
super().__init__(world_folder=world_folder, **kwargs)
def assign_role(self, profile: NodeProfile, is_world_master: bool):
# The first world master becomes the broadcaster; everyone else is a user
if is_world_master and len(self.world_masters) <= 1:
return "broadcaster"
return "user"
def create_behav_files(self):
# ---- Role "user": connect, then listen & respond ----
from .user import WAgent as UserAgent
behav = HybridStateMachine(UserAgent(proc=None))
behav.set_role("user")
behav.set_welcome_message("🗯️ A simple chatroom for humans and AIs.")
behav.add_state("init", blocking=True)
behav.add_state("waiting_handshake", blocking=False)
behav.add_state("message_sent", blocking=False)
behav.add_state("ready", action="check_messages",
args={"max_silence_seconds": 25.0, "talk_probability": 0.01,
"history_len": 3},
msg="👍 Ready!")
behav.add_transit("init", "waiting_handshake",
action="connect_to_broadcaster", args={"role": "broadcaster"},
msg="🔗 Connecting to the room...")
behav.add_transit("waiting_handshake", "ready",
action="connected", args={"handshake_completed": True})
behav.add_transit("ready", "message_sent",
action="generate_and_send", args={"samples": 1},
ready=True, avoid_changing_ready=True)
behav.add_transit("message_sent", "ready", action="nop")
behav.save(os.path.join(self.world_folder, "user.json"))
# ---- Role "broadcaster": relay every message to everyone ----
from .broadcaster import WAgent as BroadcasterAgent
behav = HybridStateMachine(BroadcasterAgent(proc=None))
behav.set_role("broadcaster")
behav.add_state("ready", blocking=True)
behav.add_transit("ready", "ready", action="broadcast_message", args={}, ready=False)
behav.save(os.path.join(self.world_folder, "broadcaster.json"))
Notice each role is built against its own agent class (UserAgent,
BroadcasterAgent). The HSM only validates that the actions it references
(connect_to_broadcaster, generate_and_send, broadcast_message, …) actually
exist on that class.
Do · the role agent classes¶
Built-in actions (connected, nop, process, send, …) come for free. The
custom ones your behaviors reference live on a small Agent subclass, each
decorated with @action.
from unaiverse.agent import Agent, action
class WAgent(Agent):
@action
async def connect_to_broadcaster(self, role: str) -> bool:
"""Find our own output stream, then connect to the broadcaster."""
self._user_stream = self.get_stream("processor", data_type="text")
if self._user_stream is None:
return False
return await self.connect_by_role(role)
@action
async def check_messages(self, max_silence_seconds: float = 10.0,
talk_probability: float = 0.333,
history_len: int = 3) -> bool:
"""Read the broadcaster's stream, decide whether to reply (or break a
silence), and stage the prompt into stdin for the next transition."""
... # full body in the chat example
return True
@action
async def generate_and_send(self, samples: int = 1) -> bool:
"""Run the local model on whatever check_messages staged, then forward
the reply to the broadcaster via a `broadcast_message` interaction."""
if not await self.process(): # stdin -> model -> stdout
return False
return await self.send(action_name="broadcast_message",
target=[self._broadcaster_peer_id],
streams=[self._my_output_hash],
num_steps=samples, copy_sys=True)
from unaiverse.agent import Agent, action
from unaiverse.interaction import Interaction
class WAgent(Agent):
@action
async def broadcast_message(self, interaction: Interaction | None = None) -> bool:
"""A custom, process-like relay. Read the sender's text from stdin
(bound, for this interaction, to the sender's output stream), prefix the
sender's name, and push it to every other user. proc stays None."""
if interaction is None:
return False
msg = self.stdin.get(uuid=interaction.uuid, requested_by="broadcast_message")
sender = self.all_agents[interaction.requester].get_static_profile()["node_name"]
others = list(set(self.world_agents.keys()) - {interaction.requester, self.get_peer_id()})
return await self.send(target=others,
data_samples={"proc_output_0": f"**{sender}:** {msg}"},
num_steps=1)
Two ways to send
generate_and_send calls send(action_name="broadcast_message", ...): "run
this action on the target." broadcast_message calls send(...) with
no action_name and a data_samples dict: a pure data push that the
recipient just stores, no transition required on their side. See
Interactions.
Do · run the world¶
Host the world in a Node, name it so others can join, and list which node
names may act as world masters.
from src.world import ChatWorld
from unaiverse.networking.node.node import Node
world = ChatWorld()
node = Node(world, node_name="ChatRoom", hidden=True, clock_delta=1. / 20.,
world_masters_node_names=["Broadcaster"])
node.run(show_senders=False)
Your world is live. Anyone can now join it the same way as in the
community path, with
node.run(join_world="ChatRoom").
Test the whole world on one machine
You don't need several terminals or a public network to try a world. Every
example ships a run_synch.py that stitches the world runner and all the
agent runners into a single synchronized process using NodeSynchronizer:
It runs the world plus every run_*.py joiner together locally, ideal for
prototyping and debugging before going online.
See · what just happened¶
graph TD
W[ChatWorld node 'ChatRoom'] -->|on join| AR[assign_role]
AR -->|"first master"| B[role: broadcaster]
AR -->|"everyone else"| U[role: user]
B -.class + behavior.- BJ[broadcaster.py + broadcaster.json]
U -.class + behavior.- UJ[user.py + user.json]
J1[Joining agent] -->|gets role + behavior| U
J2[Joining human] -->|gets role + behavior| U
A world is declarative coordination. You never wrote per-agent networking
code. You described roles (assign_role), gave each role an agent class with
its custom actions, and built
each role's behavior as a state machine serialized to JSON. When something
joins, the world picks its role and ships it the matching behavior. The joiner
runs it. Add stats and badges to score
participation, and you have a full environment.
Go deeper¶
-
Roles, the world folder, stats, badges, the full anatomy.
-
States, transitions, actions, and policies, the behavior language.
-
@action,send, and howbroadcast_messagemoves data between peers. -
Example worlds
The repo ships
chat,animal_school,cat_library,class_incremental_learning,info_extraction,signal_school, and more. Browse them.